
Добавляем пользовательскую LLM в торгового робота (Часть 5): Разработка и тестирование торговой стратегии с помощью LLM (III) – Настройка адаптера
Содержание
- Содержание
- Введение
- Настройка среды
- Создание модуля адаптера
- Переделываем класс GPT2LMHeadModel
- Настройка адаптера
- Сравнение производительности различных методов точной настройки
- Заключение
Введение
В предыдущей статье мы представили, как выполнить тонкую настройку предварительно обученной модели GPT-2 с использованием метода LoRA, и сравнили ее с полностью настроенной моделью по нескольким интересующим нас аспектам, включая, помимо прочего, затраты на обучение, на вывод и производительность модели.
В этой статье мы воспользуемся методом настройки адаптера для точной настройки предварительно обученной модели GPT-2 и сравним ее с уже представленными методами точной настройки. Конечно, мы не будем продолжать внедрять различные методы тонкой настройки больших языковых моделей, поскольку новые методы тонкой настройки появляются постоянно. Если воспроизводить каждый метод по отдельности, боюсь, у вас не хватит терпения прочитать их все, поэтому я представлю лишь несколько самых основных методов тонкой настройки (например, мы уже представили настройку LoRA и не будем время на описание настройки QLoRA - метода, являющегося расширением LoRA).
Таким образом это будет последняя статья о тонкой настройке больших языковых моделей. Если вы хотите попробовать другие методы, вы можете обратиться к логике тонкой настройки, упомянутой в этой серии статей, и применить ее к другим методам тонкой настройки, чтобы продолжить исследование. Начиная со следующей статьи, мы сосредоточимся на объединении обученной модели с разработкой советника для разработки торговых стратегий и проведения тестирования на истории.
В нашем примере мы используем относительно агрессивный подход, который заключается в вводе 20 точек данных для прогнозирования следующих 40 точек данных. Мы выбрали этот вариант, поскольку сложно сравнивать различия, если прогнозируемые значения слишком короткие. Это более агрессивный подход по сравнению с практическими приложениями, где можно было бы использовать более консервативную стратегию ввода 20 значений для прогнозирования следующих 5 значений. Важно помнить об этом при применении этих методов в торговле в реальном времени. Более практичное решение — установить эти два значения (входную и выходную длину) в качестве гиперпараметров, а затем использовать генетический алгоритм для тестирования на истории на разных валютных парах и разных периодах, чтобы найти оптимальные параметры. Мы не будем специально обсуждать это решение в данной серии статей. Читатели могут попробовать реализовать его самостоятельно.
Теперь давайте сосредоточимся на том, как использовать настройку адаптера для точной настройки предварительно обученной модели GPT-2.
Настройка среды
Ниже описывается операционная среда для примеров кода, приведенных в этой статье. Конечно, это не означает, что ваша среда должна быть такой же, как моя, но если у вас возникнут проблемы с запуском кода, вы можете обратиться к моей конфигурации среды.
Операционная система: Ubuntu 22.04.5 LTS (или соответствующая версия WSL)
Версия Python: 3.10.14
Необходимые библиотеки Python:
- torch-2.4.1
- numpy-1.26.3
- pandas-2.2.3
- transformers-4.45.1
- peft-0.13.0
- matplotlib-3.9.2
Если вы не знакомы с настройкой среды выполнения кода, я подробно описал ее в других статьях серии:
- ользователи видеокарт AMD могут обратиться к предыдущей статье: Добавляем пользовательскую LLM в торгового робота (Часть 4): Обучение собственной LLM с помощью GPU
- Пользователи видеокарт NVIDIA могут обратиться ко второй статье серии: Добавляем пользовательскую LLM в торгового робота (Часть 2): Пример развертывания среды
В данной статье эта тема подробно рассматриваться не будет.
Создание модуля адаптера
Мы кратко рассмотрели настройку адаптера (Adapter-tuning) в первой статье этого раздела. В общем случае, настройка адаптера представляет собой модульный метод точной настройки, который обеспечивает тонкую настройку путем вставки специализированных модулей адаптера в различные слои предварительно обученной модели. Каждый модуль адаптера можно рассматривать как небольшую нейронную сеть, отвечающую за сбор данных о распределении конкретной задачи. Более того, модуль Адаптера может обучаться независимо от исходной модели, что удобно для управления и оптимизации.
В то же время к одной и той же предварительно обученной модели можно легко добавить адаптеры для решения нескольких задач, чтобы добиться многозадачного обучения. Особенно когда задача сложная и объем данных ограничен, модель, настроенная с помощью настройки адаптера, может достичь более высокой производительности.
Конечно, по сравнению с LoRA модуль адаптера может вводить больше параметров, увеличивая нагрузку на хранилище и вычисления, и для каждой задачи необходимо реализовывать и настраивать соответствующий модуль адаптера, а реализация становится более сложной. Настройка LoRA в большей степени направлена на улучшение адаптивности модели с минимальным количеством параметров, что подходит для сценариев с ограниченными ресурсами и требующих эффективной тонкой настройки. С другой стороны, настройка адаптера собирает информацию, специфичную для задачи, путем внедрения независимых модулей, что подходит для сценариев, требующих многозадачного обучения или гибкой настройки.
В настоящее время, как только вы определили цель задачи, выбор правильного метода имеет решающее значение. Если обученная модель не может добиться хороших результатов, независимо от того, как вы настраиваете параметры, вам следует рассмотреть возможность изменения модели или метода обучения, а не отказываться от собственных идей.
Далее мы будем использовать настройку адаптера для пошаговой настройки модели GPT-2. Сначала мы создадим модуль адаптера и модуль GPT2LMHeadModel (а именно класс GPT2LMHeadModelWithAdapters), а затем адаптируем модуль адаптера к классу GPT2LMHeadModelWithAdapters.
Чтобы интегрировать модуль адаптера в GPT-2, мы создадим модифицированную версию класса GPT2LMHeadModel. В этом примере представлена лишь упрощенная реализация. Обратите внимание на ключевые технологии интеграции адаптера. Общая логика реализации модуля адаптера несложна. Сначала мы определяем класс, наследующий от nn.Module, который содержит две основные операции: понижающую выборку (down_project) и повышающую выборку (up_project). down_project сопоставляет входные признаки со слоем узкого места, пропускает через функцию активации ReLU и добавляет отсев для предотвращения переобучения; up_project сопоставляет признаки слоя узкого места обратно с исходным измерением и снова использует отсев для предотвращения переобучения.
Теперь давайте реализуем код. Сначала определим класс Adapter, наследующий от nn.Module Torch: class Adapter(nn.Module):
Определим метод инициализации класса, принимающий два параметра: in_features и bottleneck_features: def __init__(self, in_features, bottleneck_features=64):
- in_features - размерность входных объектов. Для модели GPT-2 это размер ее встроенного слоя.
- bottleneck_features - размер слоя "бутылочного горлышка", то есть размер объекта после слоя линейной проекции. Значение по умолчанию — 64.
- Вызовем метод инициализации родительского класса (nn.Module): super(Adapter, self).__init__()
- Определим линейный слой (nn.Linear) для уменьшения размерности входных объектов до размерности слоя "бутылочного горлышка": self.down_project = nn.Linear(in_features, bottleneck_features)
- Определим еще один линейный слой для увеличения размерности признаков от слоя узкого места обратно до размерности входных признаков: self.up_project = nn.Linear(bottleneck_features, in_features)
- Определим слой исключения (Dropout), который используется для случайного отбрасывания части нейронов во время обучения с целью предотвращения переобучения. Вероятность отбрасывания установлена равной 0.1: self.dropout = nn.Dropout(0.1)
- Вызовем метод инициализации веса: self.init_weights()
Определим метод инициализации веса init_weights():
- Инициализируем параметры веса слоя down_project, используя нормальное распределение со средним значением 0,0 и стандартным отклонением 0.02: nn.init.normal_(self.down_project.weight, mean=0.0, std=0.02)
- Инициализируем параметры смещения слоя down_project константой 0: nn.init.constant_(self.down_project.bias, 0)
- Аналогичным образом инициализируем параметры веса слоя up_project, используя нормальное распределение со средним значением 0,0 и стандартным отклонением 0,02: nn.init.normal_(self.up_project.weight, mean=0.0, std=0.02)
- Инициализируем параметры смещения слоя up_project константой 0: nn.init.constant_(self.up_project.bias, 0)
Определим метод прямого распространения forward(): def forward(self, hidden_states), который принимает один параметр hidden_states
- Проецируем входные скрытые состояния на измерение слоя бутылочного горлышка через линейный слой down_project: hidden_states = self.down_project(hidden_states)
- Выполним нелинейное преобразование скрытых состояний слоя бутылочного горлышка с помощью функции активации ReLU: hidden_states = F.relu(hidden_states)
- Применяем Dropout к нелинейно преобразованным скрытым состояниям, случайным образом отбрасывая часть нейронов: hidden_states = self.dropout(hidden_states)
- Увеличиваем скрытые состояния от измерения слоя узкого места обратно до измерения входных признаков через линейный слой up_project: hidden_states = self.up_project(hidden_states)
- Снова применяем Dropout к скрытым состояниям с повышенной выборкой: hidden_states = self.dropout(hidden_states)
- Наконец, возвращаем скрытые состояния, обработанные модулем адаптера: return hidden_states
Полный класс адаптера:
class Adapter(nn.Module): def __init__(self, in_features, bottleneck_features=64): super(Adapter, self).__init__() self.down_project = nn.Linear(in_features, bottleneck_features) self.up_project = nn.Linear(bottleneck_features, in_features) self.dropout = nn.Dropout(0.1) self.init_weights() def init_weights(self): nn.init.normal_(self.down_project.weight, mean=0.0, std=0.02) nn.init.constant_(self.down_project.bias, 0) nn.init.normal_(self.up_project.weight, mean=0.0, std=0.02) nn.init.constant_(self.up_project.bias, 0) def forward(self, hidden_states): hidden_states = self.down_project(hidden_states) hidden_states = F.relu(hidden_states) hidden_states = self.dropout(hidden_states) hidden_states = self.up_project(hidden_states) hidden_states = self.dropout(hidden_states) return hidden_states
Таким образом, мы просто создали модуль адаптера. Следующим шагом будет адаптация этого модуля к нашей модели GPT-2, поэтому нам нужно переписать класс GPT2LMHeadModel.
Переделываем класс GPT2LMHeadModel
Если мы будем полностью переписывать класс GPT2LMHeadModel, проект раздуется до огромных размеров. Мы приводим здесь лишь упрощенную версию в качестве примера и реализуем только ключевые части. Наша задача здесь — приспособить модуль адаптера к сети GPT-2 и обрабатывать различные входные условия и выходные требования модели. После инициализации нам также необходимо переписать функцию прямого распространения forward(), вызвать слой преобразователя исходной модели GPT-2 для получения скрытого состояния hidden_states, а затем по очереди применить каждый модуль адаптера, добавляя выход модуля адаптера к исходному скрытому состоянию. Наконец, окончательные логиты генерируются посредством линейного слоя языковой модели (lm_head) и рассчитываются потери. Теперь давайте завершим код.
Мы определяем наш переписанный класс как GPT2LMHeadModelWithAdapters, наследующий от GPT2LMHeadModel: class GPT2LMHeadModelWithAdapters(GPT2LMHeadModel)
Определим метод инициализации __init__() класса GPT2LMHeadModelWithAdapters и вызовем метод инициализации родительского класса в методе инициализации для добавления адаптеров:
- Определите метод класса __init__(self, config), который получает параметр конфигурации config: def __init__(self, config):
- Вызовем метод инициализации родительского класса: super().__init__(config)
- Инициализируем адаптеры, тип — nn.ModuleList, содержащий модули адаптеров, количество которых совпадает с количеством слоев модели GPT-2, где config.n_embd — размер слоя внедрения, а config.n_layer — количество слоев: self.adapters = nn.ModuleList([Adapter(config.n_embd) for _ in range(config.n_layer)])
Затем реализуем метод прямого распространения forward() в классе GPT2LMHeadModelWithAdapters:
- Определим метод прямого распространения, приняв необходимые нам параметры, которые используются для управления поведением и форматом входных данных модели (мы не будем вводить эти параметры здесь по одному, заинтересованные читатели могут попробовать оптимизировать эти параметры): def forward(self, input_ids=None, past_key_values=None, attention_mask=None, token_type_ids=None, position_ids=None, head_mask=None, inputs_embeds=None, encoder_hidden_states=None, encoder_attention_mask=None, labels=None, use_cache=None, output_attentions=None, output_hidden_states=None, return_dict=None,):
- Затем вызовем слой transformer в модели для прямого распространения, получим выходные данные модели и передадим их переменной transformer_outputs: transformer_outputs = self.transformer(input_ids, past_key_values=past_key_values, attention_mask=attention_mask, token_type_ids=token_type_ids, position_ids=position_ids, head_mask=head_mask, 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 слоя трансформера, который является входным параметром, который должен обработать модуль адаптера: hidden_states = transformer_outputs[0]
- Затем пройдемся по всем модулям адаптера, используя цикл for, чтобы подготовиться к следующему этапу адаптации: for i, adapter in enumerate(self.adapters):
- Добавим выход каждого слоя модуля адаптера к исходному скрытому состоянию и назначим его hidden_states в качестве нового скрытого состояния, переданного следующему слою: hidden_states = hidden_states + adapter(hidden_states)
- После обработки hidden_states нам также необходимо преобразовать обработанное скрытое состояние (hidden_states) в вывод logits языковой модели через слой lm_head модели. Каждый логит соответствует вероятности словаря: lm_logits = self.lm_head(hidden_states)
После конвертации получаем ссылку для расчета убытка:
- Инициализируем отсутствие потерь: loss = None
- Проверим наличие меток: if labels is not None:
- Удалим последний токен из вывода logits, поскольку нам нужно предсказать следующий токен: shift_logits = lm_logits[..., :-1, :].contiguous()
- Удалим последний токен метки, так как нам нужно предсказать следующий токен: shift_labels = labels[..., 1:].contiguous()
- Определим функцию потерь как кросс-энтропийную потерю (CrossEntropyLoss), которая часто используется в задачах классификации: loss_fct = nn.CrossEntropyLoss()
- Сгладим shift_logits и shift_labels (view(-1, ...)) и затем используем функцию потерь кросс-энтропии: loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))
Здесь следует особо отметить, что языковая модель обычно обучается при прогнозировании следующего слова, а не при прямом прогнозировании текущего слова. Таким образом, выходные данные модели lm_logits и метки меток необходимо сместить на одну позицию по временному шагу для точного расчета потерь. Например, в случае с предложением "I love programming" (я люблю программирование) входные данные модели могут быть "I love" (я люблю), а выходные данные модели lm_logits должны представлять собой распределение вероятностей, соответствующее фразе "love programming" (люблю программирование). Чтобы рассчитать потери, нам нужно сопоставить распределение вероятностей "love programming" (люблю программирование) с меткой "programming" (программирование).
- Проверим конфигурацию return_dict. При False вычислим и объединим выходные данные: if not return_dict:
- Объединим выход функции logits с другими выходами слоя трансформера (за исключением первого выхода скрытого состояния) в выходной параметр: output = (lm_logits,) + transformer_outputs[1:]
- Если метки предоставлены и потери рассчитаны, потери возвращаются вместе с выходными данными, в противном случае возвращаются только выходные данные: return ((loss,) + output) if loss is not None else output
- Если return_dict установлен в True, вернем причинный вывод напрямую: 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, )
Таким образом мы адаптировали модуль адаптера к нашему классу GPT2LMHeadModelWithAdapters. Но, какя уже говорил, это простой пример. В реальных сценариях применения тщательно проектируйте связанные модули в соответствии с требованиями задачи.
Настройка адаптера
Мы создали класс адаптера (Adapter) и класс модели GPT-2 GPT2LMHeadModelWithAdapters, приспособленный с помощью модуля адаптера. Загрузим модель и данные, чтобы начать тонкую настройку. Некоторые коды, которые были интерпретированы в оригинальной статье, здесь подробно не интерпретируются. При необходимости вернитесь к предыдущим статьям.
1. Подготовка
Импортируем необходимые библиотеки, здесь ничего особенного.
import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer from transformers import TextDataset, DataCollatorForLanguageModeling from transformers import Trainer, TrainingArguments, modeling_outputs import torch from torch import nn import torch.nn.functional as F
Если в системе есть доступный графический процессор (проверяется torch.cuda.is_available()), используйте графический процессор, в противном случае используйте центральный процессор. Определите загруженную модель и имя настроенной модели.
dvc = 'cuda' if torch.cuda.is_available() else 'cpu' print(dvc) model_name_or_path = 'gpt2' Tuned_model = "gpt2_Adapter-tuning"
2. Загрузка данных и токенизатор
Не забудьте поместить сюда созданный нами модуль адаптера и переписанный класс GPT2LMHeadModelWithAdapters. Вы также можете поместить их в другие скрипты, а затем импортировать в скрипт обучения.
Прочитаем данные из файла llm_data.csv и создадим объект DataFrame, который используется для тестирования настроенной модели.
df = pd.read_csv('llm_data.csv')
Загрузим предварительно обученный токенизатор GPT-2.
tokenizer = GPT2Tokenizer.from_pretrained(model_name_or_path)
Создадим объект набора обучающих данных, укажем используемый токенизатор с помощью параметра tokenizer, укажем путь к файлу обучающих данных с помощью параметра file_path и размер блока 60 с помощью block_size=60. Обратите внимание, что это значение не может быть установлено произвольно и должно соответствовать данным набора.
train_dataset = TextDataset(tokenizer=tokenizer, file_path="train.txt", block_size=60)
Объединим несколько выборок данных в один пакет и одновременно выполните моделирование маскированного языка (masked language modeling, MLM). Используем параметр tokenizer для указания используемого токенизатора и mlm=False для указания того, что моделирование маскированного языка (MLM) не используется, а используется моделирование причинного языка (causal language modeling, CLM).
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
3. Загрузим модель и настроим ее
Сначала используем класс TrainingArguments для создания экземпляра объекта параметров обучения.
training_args = TrainingArguments(output_dir=Tuned_model, overwrite_output_dir=True, num_train_epochs=3, per_device_train_batch_size=32, save_strategy='no', )
- output_dir=Tuned_model указывает выходной каталог обучения как gpt2_Adapter-tuning.
- overwrite_output_dir=True - нужно ли перезаписать, если выходной каталог уже существует.
- num_train_epochs=3 - задать количество эпох обучения, равное 3.
- per_device_train_batch_size=32 - указать размер обучающего пакета для каждого устройства, равный 32.
- save_strategy='no' - указать, что контрольные точки не нужно сохранять.
Загрузим и создадим экземпляр предварительно обученного объекта модели GPT-2 с помощью модуля адаптера:
model = GPT2LMHeadModelWithAdapters.from_pretrained(model_name_or_path) trainer = Trainer(model=model, args=training_args, data_collator=data_collator, train_dataset=train_dataset,)
- model=model - указать модель обучения.
- args=training_args - определить параметры обучения.
- data_collator=data_collator - указать сборщик данных.
- train_dataset=train_dataset - указать набор данных для обучения.
Используем метод train() объекта Trainer, чтобы начать обучение: trainer.train()
trainer.train()
Сохраним настроенную модель после обучения: trainer.save_model(Tuned_model)
trainer.save_model(Tuned_model)
После точной настройки модель будет сохранена в папке gpt2_Adapter-tuning в файле, где находится скрипт обучения.
4. Протестируем точно настроенную модель
После точной настройки нам необходимо загрузить настроенную модель и выполнить вывод, чтобы проверить, может ли настроенная модель работать нормально. Конечно, при загрузке настроенной модели нам необходимо использовать наш переписанный класс GPT2LMHeadModelWithAdapters. После загрузки модели нам также необходимо включить ускорение GPU и перевести модель в режим вывода.
model = GPT2LMHeadModelWithAdapters.from_pretrained(Tuned_model) model.to(dvc) model.eval()
Следующий шаг — проверка вывода, чтобы убедиться, что модель работает правильно. Этот процесс аналогичен описанному в предыдущей статье. Подробную интерпретацию кода можно найти в предыдущей статье.
prompt = ' '.join(map(str, df.iloc[:, 1:20].values[-1])) generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors='pt').to(dvc), do_sample=True, max_length=200)[0], skip_special_tokens=True) print(generated)
И вот результат:
Полный код скрипта тонкой настройки - lora-tuning.py:
import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer from transformers import TextDataset, DataCollatorForLanguageModeling from transformers import Trainer, TrainingArguments,modeling_outputs import torch from torch import nn import torch.nn.functional as F dvc = 'cuda' if torch.cuda.is_available() else 'cpu' print(dvc) model_name_or_path = 'gpt2' Tuned_model="gpt2_Adapter-tuning" # Define the Adapter module class Adapter(nn.Module): def __init__(self, in_features, bottleneck_features=64): super(Adapter, self).__init__() self.down_project = nn.Linear(in_features, bottleneck_features) self.up_project = nn.Linear(bottleneck_features, in_features) self.dropout = nn.Dropout(0.1) self.init_weights() def init_weights(self): nn.init.normal_(self.down_project.weight, mean=0.0, std=0.02) nn.init.constant_(self.down_project.bias, 0) nn.init.normal_(self.up_project.weight, mean=0.0, std=0.02) nn.init.constant_(self.up_project.bias, 0) def forward(self, hidden_states): hidden_states = self.down_project(hidden_states) hidden_states = F.relu(hidden_states) hidden_states = self.dropout(hidden_states) hidden_states = self.up_project(hidden_states) hidden_states = self.dropout(hidden_states) return hidden_states # Integrate the Adapter into the model class GPT2LMHeadModelWithAdapters(GPT2LMHeadModel): def __init__(self, config): super().__init__(config) self.adapters = nn.ModuleList([Adapter(config.n_embd) for _ in range(config.n_layer)]) def forward( self, input_ids=None, past_key_values=None, attention_mask=None, token_type_ids=None, position_ids=None, head_mask=None, inputs_embeds=None, encoder_hidden_states=None, encoder_attention_mask=None, labels=None, use_cache=None, output_attentions=None, output_hidden_states=None, return_dict=None, ): transformer_outputs = self.transformer( input_ids, past_key_values=past_key_values, attention_mask=attention_mask, token_type_ids=token_type_ids, position_ids=position_ids, head_mask=head_mask, inputs_embeds=inputs_embeds, encoder_hidden_states=encoder_hidden_states, encoder_attention_mask=encoder_attention_mask, use_cache=use_cache, output_attentions=output_attentions, output_hidden_states=output_hidden_states, return_dict=return_dict, ) hidden_states = transformer_outputs[0] # Apply adapters for i, adapter in enumerate(self.adapters): hidden_states = hidden_states + adapter(hidden_states) lm_logits = self.lm_head(hidden_states) loss = None if labels is not None: # Shift so that tokens < n predict the next token shift_logits = lm_logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() # Flatten the tokens loss_fct = nn.CrossEntropyLoss() loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1)) if not return_dict: output = (lm_logits,) + transformer_outputs[1:] return ((loss,) + output) if loss is not None else output return modeling_outputs.CausalLMOutputWithCrossAttentions( loss=loss, logits=lm_logits, past_key_values=transformer_outputs.past_key_values, hidden_states=transformer_outputs.hidden_states, attentions=transformer_outputs.attentions, cross_attentions=transformer_outputs.cross_attentions, ) if __name__=="__main__": # Load data df = pd.read_csv('llm_data.csv') tokenizer = GPT2Tokenizer.from_pretrained(model_name_or_path) train_dataset = TextDataset(tokenizer=tokenizer, file_path="train.txt", block_size=60) data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False) training_args = TrainingArguments(output_dir=Tuned_model, overwrite_output_dir=True, num_train_epochs=3, per_device_train_batch_size=32, save_strategy= 'no', ) # Initialize model with adapters model = GPT2LMHeadModelWithAdapters.from_pretrained(model_name_or_path) trainer = Trainer(model=model, args=training_args, data_collator=data_collator, train_dataset=train_dataset,) trainer.train() trainer.save_model(Tuned_model) # Load the model for inference model = GPT2LMHeadModelWithAdapters.from_pretrained(Tuned_model) model.to(dvc) model.eval() prompt = ' '.join(map(str, df.iloc[:, 1:20].values[-1])) generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors='pt').to(dvc), do_sample=True, max_length=200)[0], skip_special_tokens=True) print(f"test the model:{generated}")
Файлы данных прикреплены в конце статьи. Исходный файл данных — llm_data.csv, а предварительно обработанный файл данных — train.txt.
Сравнение производительности различных методов точной настройки
Далее мы сравним эффективность и производительность различных методов точной настройки. До сих пор мы внедрили только тонкую настройку по всем параметрам и тонкую настройку LoRA. Если добавить в эту статью настройку адаптера, то всего их три. Далее мы будем только их сравнивать.
1. Сравнение эффективности
Обучение (LoRA):
- train_runtime: 69.5605s
- VRAM: 4.1G
- generate_runtime: 1.242877s
Обучение с полной настройкой параметров:
- train_runtime: 101.7946s
- VRAM: 5.67G
- generate_runtime: 0.876525s
Обучение (адаптер):
- train_runtime: 104.4355s
- VRAM: 5.52G
- generate_runtime: 0.882792s
Время обучения (сек) | Видеопамять (ГБ) | Время генерации (сек) | |
---|---|---|---|
Полная тонкая настройка параметров | 101.7946 | 5.67 | 0.876525 |
Настройка LoRA | 69.5605 | 4.1 | 1.242877 |
Настройка адаптера | 104.4355 | 5.52 | 0.882792 |
2. Сравнение точности
Как и в предыдущей статье, мы по-прежнему загружаем первые 20 столбцов цен закрытия в последней строке исходных данных в качестве входных данных, а оставшиеся данные — в качестве результата для оценки моделей, полученных двумя методами обучения. Как уже упоминалось в начале статьи, для большей достоверности сравнения результатов мы выбрали более агрессивную длину прогноза.
Первые 20 цен закрытия:
- 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]
Оставшиеся цены закрытия:
- 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]
Далее мы загружаем модели по отдельности (параметры модели полной настройки параметров сохраняются в папке gpt2_stock в текущем каталоге, модель тонкой настройки LoRA сохраняется в папке gpt2_LORA_None в текущем каталоге, а модель настройки адаптера сохраняется в папке gpt2_Adapter-tuning в текущем каталоге), запускаем вывод и рассчитываем их MSE, RMSE и NRMSE в соответствии с полученными результатами. Эти коды были представлены в предыдущей статье, и в этой статье мы не будем их подробно описывать. Полный код тестового скрипта — test.py:
import time import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer, GPT2Config from sklearn.metrics import mean_squared_error import torch import numpy as np from peft import PeftModel import matplotlib.pyplot as plt from adapter_tuning import GPT2LMHeadModelWithAdapters df = pd.read_csv('llm_data.csv') dvc='cuda' if torch.cuda.is_available() else 'cpu' base_model='gpt2' fine_tuning_path='./gpt2_stock' lora_tuning_path ='./gpt2_LORA_None' adpter_tuning_path='./gpt2_Adapter-tuning' pre_length=40 tokenizer = GPT2Tokenizer.from_pretrained(base_model) model_fine_tuning = GPT2LMHeadModel.from_pretrained(fine_tuning_path).to(dvc) model_lora_tuning = GPT2LMHeadModel.from_pretrained(base_model) model_lora_tuning=PeftModel.from_pretrained(model_lora_tuning, lora_tuning_path).to(dvc) model_adapter_tuning = GPT2LMHeadModelWithAdapters.from_pretrained(adpter_tuning_path).to(dvc) input_data=df.iloc[:,1:20].values[-1] true_prices= df.iloc[-1:,21:].values.tolist()[0] prompt = ' '.join(map(str, input_data)) def generater(model): global true_prices model.eval() token=tokenizer.encode(prompt, return_tensors='pt').to(dvc) start_=time.time() generated = tokenizer.decode(model.generate(token, do_sample=True, max_length=200)[0], skip_special_tokens=True) end_=time.time() print(f'generate time:{end_-start_}') generated_prices=generated.split('\n')[0] generated_prices=list(map(float,generated_prices.split())) generated_prices=generated_prices[0:pre_length] # def trim_lists(a, b): # min_len = min(len(a), len(b)) # return a[:min_len], b[:min_len] # true_prices,generated_prices=trim_lists(true_prices,generated_prices) print(f"input data:{input_data}") print(f"true prices:{true_prices}") print(f"generated prices:{generated_prices}") mse = mean_squared_error(true_prices[:pre_length], generated_prices) print('MSE:', mse) rmse=np.sqrt(mse) nrmse=rmse/(np.max(true_prices)-np.min(generated_prices)) print(f"RMSE:{rmse},NRMSE:{nrmse}") return generated_prices, mse, rmse, nrmse def plot_(a,b,c,title): plt.figure(figsize=(7, 6)) if title=='predication': plt.plot(true_prices[:pre_length], label='True Values', marker='o') plt.plot(a, label='fine_tuning', marker='x') plt.plot(b, label='lora_tuning', marker='s') plt.plot(c,label='adapter_tuning',marker='d') plt.title(title) plt.xlabel('Index') plt.ylabel('Value') plt.legend() plt.savefig(f"{title}.png") def groups_chart(a,b,c,models): metrics = ['Train_time(s)', 'Infer_time(s)', 'Memory(GB)', 'MSE', 'RMSE', 'NRMSE'] plt.figure(figsize=(7, 6)) a=[101.7946,1.243,5.67,a[1],a[2],a[3]] b=[69.5605,0.877,4.10,b[1],b[2],b[3]] c=[104.4355,0.883,5.52,c[1],c[2],c[3]]# 104.4355s,VRAM:5.52G generate_runtime:0.882792s bar_width = 0.2 r1 = np.arange(len(metrics)) r2 = [x + bar_width for x in r1] r3 = [x + bar_width for x in r2] plt.bar(r1, a, color='r', width=bar_width, edgecolor='grey', label=models[0]) plt.bar(r2, b, color='b', width=bar_width, edgecolor='grey', label=models[1]) plt.bar(r3, c, color='g', width=bar_width, edgecolor='grey', label=models[2]) plt.yscale('log') plt.xlabel('Metrics', fontweight='bold') plt.xticks([r + bar_width for r in range(len(metrics))], metrics) plt.ylabel('Values (log scale)', fontweight='bold') plt.title('Model Comparison') plt.legend() # plt.show() plt.savefig('Comparison.png') fine_tuning_result = generater(model_fine_tuning) lora_tuning_result = generater(model_lora_tuning) adapter_tuning_result=generater(model_adapter_tuning) plot_(fine_tuning_result[0],lora_tuning_result[0],adapter_tuning_result[0],title='predication') groups_chart(fine_tuning_result,lora_tuning_result,adapter_tuning_result,models=['fine-tuning','lora-tuning','adapter-tuning'])
Примечание:
Порядок величины измеряемых нами показателей не одинаков, поэтому я использовал здесь логарифмическую шкалу: plt.yscale('log'), это поможет эффективно справиться с ситуацией, когда величина данных существенно различается.
Результат вывода модели точной настройки со всеми параметрами:
- сгенерированные цены:[0.61163, 0.61162, 0.61191, 0.61195, 0.61209, 0.61231, 0.61224, 0.61207, 0.61187, 0.61184, 0.6119, 0.61169, 0.61168, 0.61162, 0.61181, 0.61184, 0.61184, 0.6118, 0.61176, 0.61165, 0.61169, 0.61186, 0.61171, 0.61171, 0.6116, 0.61165, 0.61168, 0.61165, 0.61169, 0.61173, 0.61184, 0.61176, 0.61171, 0.61176, 0.61171, 0.61207, 0.61208, 0.61202, 0.6117, 0.61207]
- MSE: 1.257374999999991e-07
- RMSE:0.00035459483921794336
- NRMSE:0.43243273075362537
Результаты вывода модели тонкой настройки LoRA:
- сгенерированные цены:[0.61163, 0.61162, 0.61191, 0.61195, 0.61209, 0.61231, 0.61224, 0.61207, 0.61187, 0.61184, 0.6119, 0.61169, 0.61168, 0.61162, 0.61181, 0.61184, 0.61184, 0.6118, 0.61176, 0.61191, 0.61187, 0.6121, 0.61187, 0.61193, 0.61195, 0.61176, 0.61194, 0.61171, 0.61198, 0.61171, 0.61171, 0.61198, 0.61172, 0.61202, 0.6116, 0.61173, 0.61199, 0.61169, 0.61171, 0.61171]
- MSE: 1.0161999999999925e-07
- RMSE:0.0003187789202566557
- NRMSE:0.3887547808008319
Результаты вывода модели настройки адаптера:
- сгенерированные цены:[0.61163, 0.61162, 0.61191, 0.61195, 0.61209, 0.61231, 0.61224, 0.61207, 0.61187, 0.61184, 0.6119, 0.61169, 0.61168, 0.61162, 0.61181, 0.61184, 0.61184, 0.6118, 0.61176, 0.61173, 0.61168, 0.61165, 0.61178, 0.61173, 0.61164, 0.61174, 0.61163, 0.61174, 0.61163, 0.61174, 0.61162, 0.61162, 0.61167, 0.61168, 0.61165, 0.61167, 0.61168, 0.61162, 0.61167, 0.61174]
- MSE: 1.5644499999999023e-07
- RMSE:0.00039553128826932293
- NRMSE:0.4944141103367081
Визуализация графиков для сравнения:
Заключение
В этой статье мы обсудили, как использовать метод настройки адаптера для точной настройки предварительно обученной модели GPT-2, а также провели горизонтальное сравнение методов точной настройки, что позволяет нам интуитивно выбирать метод обучения и модель, которые больше подходят для нашей торговой стратегии.
Мы обнаружили, что хотя настройка адаптера может потребовать немного больше времени на обучение и большего объема видеопамяти, чем LoRA, она предлагает иной подход к сбору информации, специфичной для задачи. Выбор оптимального метода зависит от конкретных требований проекта и имеющихся ресурсов. Полная точная настройка параметров остается надежной базой, в то время как LoRA обеспечивает эффективность, а настройка адаптера обеспечивает модульность и потенциальные преимущества для многозадачных сценариев.
В следующих статьях мы больше не будем пробовать различные методы тонкой настройки. Мы используем нашу точно настроенную модель для формулирования торговых стратегий и интеграции их в советник. Затем мы проведем обратное тестирование и оценим советника. Если вы заинтересованы в тонкой настройке модели и хотите получить лучшие результаты, вы можете попробовать следовать моим идеям и завершить ее шаг за шагом в соответствии с примером кода. Поверьте мне, это несложно.
Увидимся в следующей статье!
Приложения:
Файлы | Описание |
---|---|
adapter_tuning.py | Код для настройки адаптера |
test.py | Код для эффективности и производительности различных методов точной настройки |
llm_data.csv | Файл необработанных данных |
train.txt | Файл данных для обучения |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/13500
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Зачем вам нужна обратная выборка до исходного размера входных данных сразу после обратной выборки? Объяснение работы слоев выглядит идентично (отсев для предотвращения избыточной подгонки), и если данные хорошо помещаются в меньший контейнер с той же функциональностью, то обратная апсемплинг выглядит излишней и расточительной (по крайней мере, вы не получаете новой информации от преобразования).
PS. Автоматический перевод сообщения с английского на (как минимум) русский выглядит нелепо, поэтому, пожалуйста, читайте оригинальное сообщение.