Реализация LoRA с нуля’.

Построение сети LoRA с нуля

Как реализовать LoRA с нуля и некоторые практические советы

Абстрактное художественное изображение LoRA, созданное DALLE

В этом блоге я покажу вам, как реализовать LoRA с нуля.

LoRA, акроним для Low-Rank Adaptation или Low-Rank Adaptors, предлагает эффективный и легкий способ настройки предварительно обученных языковых моделей. Это включает в себя маскированные языковые модели, такие как BERT и RoBERTa, а также причинные (или чат-ботные) модели, такие как GPT, Llama и Mistral.

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

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

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

Мы начнем с того, чтобы изучить, как работает LoRA, затем я покажу вам, как разработать его с нуля для модели RoBERTa, после чего мы оценим нашу реализацию с использованием тестов GLUE и SQuAD, а также обсудим общие советы и улучшения.

Как работает LoRA

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

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

Очевидно, что если у W-orig размерность n×m и мы просто инициализируем новую матрицу дельты с теми же размерностями для настройки, мы ничего бы не получили; наоборот, мы удвоили бы количество параметров.

Трюк состоит в том, чтобы сделать ΔW менее “размерного” по сравнению с оригинальной матрицей, конструируя его путем матричного умножения от более низкоразмерных матриц B и A.

Где мы сначала определяем ранг r, который значительно меньше базовых размерностей r≪n и r≪m. Затем матрица B имеет размерность n×r, а матрица A – размерность r×m. Перемножение их даёт матрицу с теми же размерностями, что и W, но созданную с использованием гораздо меньшего количества параметров.

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

Например, это может выглядеть следующим образом:

Иллюстрация примера того, как может выглядеть LoRA для фактической матрицы

Представьте ситуацию, когда наша базовая размерность составляет 1024, а мы выбрали ранг LoRA равным 4, тогда:

  • W имеет 1024 * 1024 ≈ 1 миллион параметров
  • A и B имеют по r * 1024 = 4 * 1024 ≈ 4K параметров каждый, общая сумма составляет 8K
  • Таким образом, нам нужно обучить только 0,8% параметров, чтобы обновить нашу матрицу с помощью LoRA

Несколько слов о LoRA: в статье LoRA они взвешивают матрицу дельта с помощью параметра альфа:

Если вы просто установите α в первое значение r, с которым вы экспериментируете, и тонко настроите скорость обучения, вы в целом можете изменить параметр r позже, не внося изменений в скорость обучения (по крайней мере, в приближенном виде). В то время как мы можем пренебречь этой деталью в нашей реализации, это общая особенность многих других библиотек LoRA, таких как PEFT от Hugging Face.

Реализация LoRA

Для нашей реализации мы хотим тесно придерживаться оригинальной статьи LoRA. В ней они тестировали, какие матрицы трансформера действительно нужно заменить. Они обнаружили, что при сравнении разных стратегий на задаче GPT-3 fine-tune достаточно приспособить только запросы и векторы значений самого механизма самовнимания.

Обратите внимание, что многие люди игнорируют это оценку в наше время и позволяют каждой матрице настраиваться вне зависимости от задачи или модели (см. статью QLoRA).

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

Для этой статьи я немного упростил код, чтобы его было проще читать, но в то же время показывая основные элементы. Полный код и некоторые обученные веса LoRA можно найти здесь: https://github.com/Montinger/Transformer-Workbench.

Реализация модели самовнимания

Модель, которую мы хотим адаптировать, – это модель RoBERTa от Huggingface. Самый простой способ – просто обернуть оригинальный механизм самовнимания RobertaSelfAttention. Новый класс LoraRobertaSelfAttention будет инициализировать матрицы LoRA. Все матрицы B инициализируются нулями, а все матрицы A – случайными числами из нормального распределения.

class LoraRobertaSelfAttention(RobertaSelfAttention):    """    Расширяет RobertaSelfAttention с матрицами LoRA (адаптация низкого ранга).    LoRA повышает эффективность, обновляя только матрицы запроса и значений.    Этот класс добавляет матрицы LoRA и применяет логику LoRA в методе forward.    Параметры:    - r (int): Ранг матриц LoRA.    - Конфигурация модели Roberta.    """    def __init__(self, r=8, *args, **kwargs):        super().__init__(*args, **kwargs)        d = self.all_head_size        # Инициализация матриц LoRA для запросов и значений        self.lora_query_matrix_B = nn.Parameter(torch.zeros(d, r))        self.lora_query_matrix_A = nn.Parameter(torch.randn(r, d))        self.lora_value_matrix_B = nn.Parameter(torch.zeros(d, r))        self.lora_value_matrix_A = nn.Parameter(torch.randn(r, d))

С учетом этих матриц мы теперь определяем новые методы класса lora_query и lora_value. Они вычисляют матрицу ΔW, то есть BA, и добавляют ее к исходной матрице, на которую мы ссылаемся из оригинальных методов query и value.

class LoraRobertaSelfAttention(RobertaSelfAttention):    # ...    def lora_query(self, x):        """        Применяет LoRA к компоненте запроса. Вычисляет модифицированный вывод запроса,         добавляя адаптацию LoRA к стандартному выводу запроса. Требуется заморозить         обычный линейный слой перед обучением.        """        lora_query_weights = torch.matmul(self.lora_query_matrix_B, self.lora_query_matrix_A)        return self.query(x) + F.linear(x, lora_query_weights)    def lora_value(self, x):        """        Применяет LoRA к компоненте значения. Вычисляет модифицированный вывод значения,         добавляя адаптацию LoRA к стандартному выводу значения. Требуется заморозить         обычный линейный слой перед обучением.        """        lora_value_weights = torch.matmul(self.lora_value_matrix_B, self.lora_value_matrix_A)        return self.value(x) + F.linear(x, lora_value_weights)

Теперь некрасивая часть: чтобы использовать методы, мы должны перезаписать исходную функцию forward в классе RobertaSelfAttention. Хотя это немного некорректно (см. обсуждение улучшений позже), это довольно просто. Во-первых, мы копируем исходный код forward из https://github.com/huggingface/transformers/blob/main/src/transformers/models/roberta/modeling_roberta.py. Во-вторых, мы заменяем каждый вызов query на lora_query и каждый вызов value на lora_value. Функция будет выглядеть следующим образом:

class LoraRobertaSelfAttention(RobertaSelfAttention):    # ...    def forward(self, hidden_states, *args, **kwargs):        """Скопировано из https://github.com/huggingface/transformers/blob/main/src/transformers/models/roberta/modeling_roberta.py, но заменен вызов query и value вызовами функций lora_query и lora_value. Мы просто приведем здесь краткое описание того, как это можно сделать. Измените каждый вызов self.value и self.query в текущей версии."""        # оригинальный код для query:        ## mixed_query_layer = self.query(hidden_states)        # обновленный код для LoRA:        mixed_query_layer = self.lora_query(hidden_states)        # Key не имеет LoRA, поэтому оставляем эти вызовы неизменными        key_layer = self.transpose_for_scores(self.key(hidden_states))        # оригинальный код для value:        ## value_layer = self.transpose_for_scores(self.value(hidden_states))        # обновленный код для LoRA:        value_layer = self.transpose_for_scores(self.lora_value(hidden_states))                # ... (остальной код функции forward не меняется)

Вуаля, вот она: наша реализация self-attention с использованием LoRA. Теперь единственная задача, которая осталась, это заменить модули внимания в исходной модели RoBERTa.

Замена модулей

Отлично, мы заменили self-attention своей собственной реализацией, но как мы можем вставить этот новый класс в старую модель RoBERTa? Фактически, нам нужно пройти по каждому именованному компоненту модели RoBERTa, проверить, является ли он классом RobertaSelfAttention, и если да, заменить его на LoraRobertaSelfAttention, при этом обеспечивая сохранение оригинальных матриц весов.

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

class LoraWrapperRoberta(nn.Module):    def __init__(self, task_type, num_classes=None, dropout_rate=0.1, model_id="roberta-large",                 lora_rank=8, train_biases=True, train_embedding=False, train_layer_norms=True):        """        Обертка для RoBERTa с использованием метода низкого ранга адаптации (LoRA) для различных задач NLP.        - task_type: Тип задачи NLP ('glue', 'squad_v1', 'squad_v2').        - num_classes: Количество классов для классификации (зависит от задачи).        - dropout_rate: Уровень dropout в модели.        - model_id: Идентификатор предобученной модели RoBERTa.        - lora_rank: Ранг для адаптации методом LoRA.        - train_biases, train_embedding, train_layer_norms:             Флаги, определяющие, какие параметры оставить тренируемыми после инициализации LoRA.             Пример:             model = LoraWrapperRoberta(task_type='glue')        """        super().__init__()        # 1. Инициализируем базовую модель с параметрами        self.model_id = model_id        self.tokenizer = RobertaTokenizer.from_pretrained(model_id)        self.model = RobertaModel.from_pretrained(model_id)        self.model_config = self.model.config        # 2. Добавляем слой для требуемых задач        d_model = self.model_config.hidden_size        self.finetune_head_norm = nn.LayerNorm(d_model)        self.finetune_head_dropout = nn.Dropout(dropout_rate)        self.finetune_head_classifier = nn.Linear(d_model, num_classes)        # 3. Готовим модель LoRA для обучения        self.replace_multihead_attention()        self.freeze_parameters_except_lora_and_bias()

Как вы можете видеть, в инициализации мы вызываем два вспомогательных метода:

  1. self.replace_multihead_attention: Этот метод заменяет внимание всех частей нейронной сети на наше ранее написанное LoraRobertaSelfAttention
  2. self.freeze_parameters_except_lora_and_bias: Этот метод замораживает все основные параметры для обучения таким образом, чтобы градиенты и оптимизатор применялись только к параметрам LoRA и остальным параметрам смещения и нормализации слоев, которые мы хотим продолжать обучать.
class LoraWrapperRoberta(nn.Module):    #...    def replace_multihead_attention_recursion(self, model):        """        Заменяет RobertaSelfAttention на LoraRobertaSelfAttention в модели.        Этот метод рекурсивно применяет замену ко всем дочерним компонентам.        Параметры        ----------        model : nn.Module            Изменяемый модуль или модель PyTorch.        """        for name, module in model.named_children():            if isinstance(module, RobertaSelfAttention):                # Заменить RobertaSelfAttention на LoraRobertaSelfAttention                new_layer = LoraRobertaSelfAttention(r=self.lora_rank, config=self.model_config)                new_layer.load_state_dict(module.state_dict(), strict=False)                setattr(model, name, new_layer)            else:                # Рекурсивный вызов для дочерних модулей                self.replace_multihead_attention_recursion(module)

Нам нужно рекурсивно пройти по всем частям модели, так как в PyTorch части сети могут быть собраны (и это относится к RoBERTa) в отдельный модуль PyTorch.

Теперь нам нужно заморозить все параметры, которые мы больше не хотим обучать:

class LoraWrapperRoberta(nn.Module):    #...    def freeze_parameters_except_lora_and_bias(self):        """        Замораживает все параметры модели, за исключением определенных слоев и типов, основанных на конфигурации.        Параметры LoRA, головка finetune, параметры смещения, эмбеддинги и единицы нормализации слоев могут быть настроены как обучаемые на основании настроек класса.        """        for name, param in self.model.named_parameters():            is_trainable = (                "lora_" in name or                "finetune_head_" in name or                (self.train_biases and "bias" in name) or                (self.train_embeddings and "embeddings" in name) or                (self.train_layer_norms and "LayerNorm" in name)            )            param.requires_grad = is_trainable

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

Сюжет: Есть способ, который сделал бы код более красивым и легким для обобщения на другие архитектуры сетей (поскольку наш сильно привязан к модели RoBERTa). Можете ли вы догадаться, что это может быть? У вас есть время поразмыслить над этим вопросом, пока мы не обсудим это в разделе Возможные улучшения ниже. Но пока давайте протестируем на некоторых проверочных данных, работает ли наша реализация на самых разных задачах.

Оценка результатов с помощью GLUE и SQuAD

Наша реализация теперь готова для оценки с использованием тестовых задач GLUE (General Language Understanding Evaluation) и SQuAD (Stanford Question Answering Dataset).

Тест GLUE – это набор из восьми различных NLP-задач, который оценивает всеобъемлющие способности модели языка. Он включает такие задачи, как анализ настроения, логическое следование и сходство предложений, предлагая надежную меру лингвистической приспособляемости и грамотности модели.

С другой стороны, SQuAD фокусируется на оценке моделей вопрос-ответ. Он предполагает извлечение ответов из пассажей Википедии, где модель определяет соответствующий фрагмент текста. Версия SQuAD v2 делает задачу более сложной, представляя вопросы без ответа, добавляя сложность и отражая ситуации из реальной жизни, когда модели должны распознавать отсутствие ответа в тексте.

Обратите внимание, что для следующего теста я не настраивал гиперпараметры, не проводил несколько запусков (особенно для небольших наборов данных GLUE, которые подвержены стохастическому шуму), не делал ранней остановки и не начинал с дообучения на предыдущей задаче GLUE (как часто делается для уменьшения вариабельности шума небольшого набора данных и предотвращения переобучения).

Все запуски:

  • Были выполнены с новоинициализированным внедрением LoRA с рангом 8 в модель RoBERTa-base
  • Обучение выполнялось ровно 6 эпох для каждой задачи, без ранней остановки.
  • В течение первых 2 эпох скорость обучения была линейно повышена до максимального значения, а затем линейно снижалась к нулю в течение оставшихся 4 эпох.
  • Максимальная скорость обучения для всех задач составляла 5e-4.
  • Размер пакета для всех задач составлял 16.

Модель RoBERTa-base имеет 124,6 миллиона параметров. С параметрами LoRA, смещениями и нормами слоев у нас остается только 420 тысяч размороженных параметров для обучения. Это означает, что мы фактически обучаемся только на 0,34% от исходных параметров.

Количество параметров, введенных LoRA для этих конкретных задач, незначительно и составляет всего 1,7 МБ фактического дискового пространства. Вы можете найти обученные LoRA в репозитории Git в папке Output.

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

Производительность в задачах GLUE с использованием LoRA
Производительность на наборах данных SQuAD с использованием LoRA

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

Возможные улучшения

Оценивая нашу реализацию, можно задаться вопросом: “Могло быть более эффективное, обобщаемое (т.е. применимое к другим архитектурам сетей) решение, чем перекодировка класса само-внимания и выполнение сложных замен?”

На самом деле мы могли просто реализовать обертку вокруг функции nn.Linear в PyTorch и быть более конкретными в том, какие слои мы хотим заменить им, путем проверки их имен. Аналогично, вы можете создавать обертки вокруг большинства основных слоев pytorch и иметь возможность быстро адаптировать LoRA для новых архитектур сетей. Чтобы дать краткое представление о том, как это можно сделать:

class LoraLinear(nn.Linear):    """    Расширяет линейный слой PyTorch с низкоранговой адаптацией (LoRA).    LoRA добавляет две матрицы к слою, позволяя эффективно обучать большие модели.    """    def __init__(self, in_features, out_features, r=8, *args, **kwargs):        super().__init__(in_features, out_features, *args, **kwargs)        # Инициализация матриц LoRA        self.lora_matrix_B = nn.Parameter(torch.zeros(out_features, r))        self.lora_matrix_A = nn.Parameter(torch.randn(r, in_features))                # Замораживаем исходную матрицу весов        self.weight.requires_grad = False    def forward(self, x: Tensor) -> Tensor:        # Вычисляем коррекцию весов LoRA        lora_weights = torch.matmul(self.lora_matrix_B, self.lora_matrix_A)        # Применяем исходные и скорректированные линейные преобразования        return super().forward(x) + F.linear(x, lora_weights)

Это фактически (почти) то, как библиотека huggingface PEFT (Parameter-Efficient Fine-Tuning) реализует LoRA. Для любого практического применения, где вы не пытаетесь учить, я настоятельно рекомендую использовать ее, вместо того чтобы писать свой собственный код.

Также стало довольно распространенной практикой внедрение LoRA во все линейные слои (т.е. все матрицы само-внимания и два линейных слоя для полносвязной прямой сети). Обычно хорошей идеей является обучение смещений и слоев нормализации, в дополнение к параметрам LoRA. Поскольку они уже малы, вам не понадобится низкоранговое внедрение для них.

Для сохранения памяти GPU также рекомендуется квантование исходных весов матрицы, что облегчает обучение больших моделей на заданном GPU. Это можно сделать эффективно с использованием библиотеки bits-and-bytes, теперь полностью интегрированной с Hugging Face (см. ссылки).

Подводя итог, вот пять заповедей низкорангового адаптирования в серьезной среде:

Пять заповедей низкорангового адаптирования

Если вам сложно читать надпись на вписанной в камень доске, то вот они снова в обычном тексте:

Пять заповедей адаптации низкого ранга

1. Используйте LoRA для эффективного тонкой настройки модели, сосредоточиваясь на минимизации размеров параметров.2. Используйте библиотеку PEFT для реализации LoRA, избегая необходимости сложного кодирования.3. Расширьте адаптации LoRA на все линейные слои, улучшая общие возможности модели.4. Сделайте смещения и нормы слоев обучаемыми, так как они являются важными для адаптивности модели и не требуют адаптации низкого ранга.5. Примените квантованную LoRA – QLoRA – для сохранения памяти GPU VRAM и тренировки вашей модели, позволяющей обучать более крупные модели.

Помните, что обучение с использованием QLoRA может быть немного медленнее, чем с использованием LoRA, поскольку включает в себя расквантование матриц во время каждого умножения. Например, при тонкой настройке чего-то массового, такого как Llama-7B, QLoRA требует примерно на 75% меньше VRAM, но приблизительно на 40% медленнее по сравнению со стандартным LoRA. Для более подробной информации ознакомьтесь с блог-постами, на которые я дал ссылки в сносках.

Пошаговое руководство по реализации PEFT

Давайте посмотрим, как фактически следовать нашим заповедям и реализовать лучшую версию с помощью PEFT.

Во-первых, давайте загрузим нашу модель квантованным образом. Благодаря интеграции bitsandbytes с библиотекой Huggingface transformers (введенной в мае 2023 года), это легко.

Нам нужно указать файл конфигурации, а затем загрузить модель непосредственно из huggingface с этой квантованием. Обычно лучше использовать объекты AutoModel из transformers. Трудно загрузить квантованную модель в качестве подмодуля вновь определенного более крупного объекта nn.module. Обычно лучше работать с необработанными моделями от huggingface и, следовательно, импортировать непосредственно AutoModelForSequenceClassification для задач GLUE и AutoModelForQuestionAnswering для бенчмарков SQuAD. В конфигурации мы также можем указать параметры, которые не нужно квантовать: здесь мы должны зарегистрировать классификацию heads или qa-output, поскольку мы хотим обучать их в полном объеме, то есть без LoRA, так как они были недавно инициализированы для настройки и ранее не являлись частью предобученной базовой модели.

import bitsandbytes as bnbfrom transformers import AutoModel, AutoModelForSequenceClassification, BitsAndBytesConfig# Конфигурация для загрузки квантованной моделиbnb_config = BitsAndBytesConfig(    load_in_4bit=True,  # Включить 4-битную загрузку    bnb_4bit_quant_type="nf4",    bnb_4bit_compute_dtype=torch.bfloat16,    llm_int8_skip_modules=['classifier', 'qa_outputs'],  # Пропустить эти при квантовании)# Загрузка модели из Huggingface с квантованиемmodel = AutoModelForSequenceClassification.from_pretrained('roberta-base',          torch_dtype="auto", quantization_config=bnb_config)

Вы можете проверить 4-битную загрузку, проверив модули модели и типы данных параметров:

# Проверка 4-битных элементов (Linear4bit) в слое внимания:print("Проверка 4-битных элементов (Linear4bit) в слое внимания:")print(model.roberta.encoder.layer[4].attention)print("Проверка типа данных uint8:")print(model.roberta.encoder.layer[4].attention.self.query.weight.dtype)

Теперь мы перейдем к внедрению параметров LoRA с помощью PEFT. Обратите внимание, что библиотека PEFT гораздо более гибкая, также при работе с пользовательскими моделями или другими запутанными структурами, поэтому долго, как все, что вы делаете, – это LoRA, а не QLoRA (квантование обычно является сложной частью).

Библиотека PEFT нацелена на замену модулей по их именам; поэтому нам нужно посмотреть имена параметров модели model.named_parameters(). Вот как это выглядит для неквантованной базовой модели roberta-base.

Module                                                        Parameters----------------------------------------------------------  ------------roberta.embeddings.word_embeddings.weight                     38_603_520roberta.embeddings.position_embeddings.weight                    394_752roberta.embeddings.token_type_embeddings.weight                      768roberta.embeddings.LayerNorm.weight                                  768roberta.embeddings.LayerNorm.bias                                    768roberta.encoder.layer.0.attention.self.query.weight              589_824roberta.encoder.layer.0.attention.self.query.bias                    768roberta.encoder.layer.0.attention.self.key.weight                589_824roberta.encoder.layer.0.attention.self.key.bias                      768roberta.encoder.layer.0.attention.self.value.weight              589_824roberta.encoder.layer.0.attention.self.value.bias                    768roberta.encoder.layer.0.attention.output.dense.weight            589_824roberta.encoder.layer.0.attention.output.dense.bias                  768roberta.encoder.layer.0.attention.output.LayerNorm.weight            768roberta.encoder.layer.0.attention.output.LayerNorm.bias              768roberta.encoder.layer.0.intermediate.dense.weight              2_359_296roberta.encoder.layer.0.intermediate.dense.bias                    3_072roberta.encoder.layer.0.output.dense.weight                    2_359_296roberta.encoder.layer.0.output.dense.bias                            768roberta.encoder.layer.0.output.LayerNorm.weight                      768roberta.encoder.layer.0.output.LayerNorm.bias                        768roberta.encoder.layer.1.attention.self.query.weight              589_824...roberta.encoder.layer.11.output.LayerNorm.bias                       768classifier.dense.weight                                          589_824classifier.dense.bias                                                768classifier.out_proj.weight                                         1_536classifier.out_proj.bias                                               2----------------------------------------------------------  ------------TOTAL                                                        124_647_170

Мы можем указать цели LoRA для выбора этих строк. Проверка заключается в том, содержит ли она указанную подстроку в своем полном имени. Таким образом, запись query и value эквивалентна нашей с нуля реализации выше. Для плотных слоев нам нужно быть более осторожными, так как классификатор также имеет плотный вывод. Если мы хотим донастроить другие плотные слои, нам нужно быть более конкретными с помощью intermediate.dense и output.dense.

Все параметры, которые не были внедрены с помощью параметров LoRA, автоматически замораживаются, то есть не получают обновлений градиента. Если есть какие-либо слои, которые мы хотим тренировать в их исходной форме, мы можем указать их, передав список параметров modules_to_save в конфигурацию Lora-Config. В нашем случае, мы хотим добавить LayerNorm здесь и донастроить головы для GLUE и SQuAD. Обратите внимание, что не каждый элемент списка должен соответствовать чему-то. Мы просто можем добавить classifier и qa_outputs в этот список, и затем у нас будет один конфигурационный файл, который будет корректно работать для обоих задач.

Для параметров смещения вы можете использовать удобный параметр конфигурации bias. Вы можете указать для этого все значения, чтобы переобучить все смещения всех модулей, lora_only, чтобы обучать только внедренные, или none, чтобы сохранить все смещения постоянными во время тренировки.

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

import peft# Конфигурация для внедрения LoRA через PEFT peft_config = peft.LoraConfig( r=2, # размерность ранга внедренных матриц LoRA lora_alpha=8, # параметр для масштабирования, используйте 8 для сравнения с нашей собственной реализацией target_modules=['query', 'key', 'value', 'intermediate.dense', 'output.dense'], # будьте точными по поводу плотных слоев, потому что у классификатора также есть плотные модули modules_to_save=["LayerNorm", "classifier", "qa_outputs"], # Перетренируйте LayerNorm; classifier - это голова для донастройки; qa_outputs - для SQuAD lora_dropout=0.1, # вероятность отсева для слоев bias="all", # none, all или lora_only) model = peft.get_peft_model(model, peft_config)

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

Для обучения, особенно с использованием QLoRA, выберите оптимизатор, совместимый с квантованными матрицами. Замените ваш оптимизатор torch стандартным вариантом bitsandbytes:

import torch import bitsandbytes as bnb # замените это optimizer = torch.optim.AdamW(аргументы здесь) # на этоoptimizer = bnb.optim.AdamW8bit(такие же аргументы)

Затем вы можете обучать эту модель, как и раньше, не беспокоясь явно о QLoRA во время обучения.

После завершения обучения процесс сохранения и повторной загрузки модели прост. Используйте model.save_pretrained, чтобы сохранить свою модель, указав желаемое имя файла. Библиотека PEFT автоматически создаст каталог в этом месте, где она сохраняет веса модели и файл конфигурации. Этот файл содержит основные детали, такие как базовая модель и параметры конфигурации LoRA.

Для загрузки модели используйте peft.AutoPeftModel.from_pretrained, передавая путь каталога в качестве аргумента. Важной точкой для запоминания является то, что конфигурация LoRA в настоящее время не сохраняет количество классов, для которых был инициализирован AutoModelForSequenceClassification. При использовании from_pretrained вам нужно вручную ввести это число класса в качестве дополнительного параметра. Если вы не сделаете этого, возникнет ошибка.

Восстановленная модель будет содержать исходную базовую модель с примененными адаптерами LoRA. Если вы решите интегрировать адаптеры LoRA постоянно в матрицы базовой модели, просто выполните model.merge_and_unload().

Для более практического понимания и подробных инструкций ознакомьтесь с репозиторием GitHub. Там вы найдете два файловых блокнота под названиями Train-QLoRA-with-PEFT.ipynb и Load-LoRA-Weights-PEFT.ipynb, предоставляющие пошаговый пример для обучения и загрузки моделей с использованием PEFT.

Заключение

«Мы не прекратим исследования, и результатом наших поисков будет то, что мы вернемся туда, откуда начали, и узнаем это место впервые».

— из стихотворения «Малый Гиддинг» Т. С. Элиота

Это путешествие привело нас от прямолинейной, хотя и запрограммированной реализации LoRA к более глубокому пониманию адаптеров низкого ранга, их практической реализации и тестирования показателей.

Мы исследовали альтернативную, более эффективную стратегию реализации и углубились в элегантность имеющихся библиотек, таких как PEFT, для интеграции LoRA.

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

Ссылки

Все изображения, если не указано иное, принадлежат автору.