Настройка LLAMAv2 с помощью QLora на Google Colab бесплатно

Бесплатная настройка LLAMAv2 с помощью QLora на Google Colab

 

Раньше мне хотелось свободно настраивать 7-миллиардную модель на одном графическом процессоре бесплатно на Google Colab. 23 мая 2023 года Тим Деттмерс и его команда опубликовали революционную статью[1] о настройке Квантованных Больших Языковых Моделей.

Квантованная модель – это модель, веса которой представлены в типе данных, который ниже типа данных, на котором она была обучена. Например, если вы обучаете модель в формате 32-битного числа с плавающей точкой, а затем преобразуете эти веса в более низкий тип данных, такой как 16/8/4-битное число с плавающей точкой, чтобы минимизировать или устранить влияние на производительность модели.

   

Мы не будем подробно рассматривать теорию квантования здесь. Вы можете обратиться к отличной статье блога Hugging-Face[2][3] и отличному видео на YouTube[4] Тима Деттмерса, чтобы понять основы теории.

Вкратце, QLora означает:

Настройка Квантованных Больших Языковых моделей с использованием Матриц Адаптации Низкого Ранга (LoRA)[5]

Давайте перейдем непосредственно к коду:

 

Подготовка данных

 

Важно понимать, что большие языковые модели предназначены для выполнения инструкций, что было впервые представлено в статье ACL 2021 года[6]. Идея проста: мы даём языковой модели инструкцию, и она следует этой инструкции и выполняет задачу. Поэтому набор данных, на котором мы хотим настроить нашу модель, должен быть в формате инструкции, иначе мы можем его преобразовать.

Один из распространенных форматов – это формат инструкции. Мы будем использовать Шаблон Промта Alpaca[7], который выглядит следующим образом:

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

### Инструкция:
{instruction}

### Ввод:
{input}

### Ответ:
{response}

 

Мы будем использовать набор данных SNLI, который представляет собой набор данных, содержащий 2 предложения и отношение между ними: противоречие, следование друг другу или нейтральное отношение. Мы будем использовать его для генерации противоречия для предложения с помощью LLAMAv2. Мы можем загрузить этот набор данных с помощью pandas.

import pandas as pd

df = pd.read_csv('snli_1.0_train_matched.csv')
df['gold_label'].value_counts().plot(kind='barh')

 

 

Здесь мы видим несколько примеров случайного противоречия.

df[df['gold_label'] == 'contradiction'].sample(10)[['sentence1', 'sentence2']]

 

 

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

def convert_to_format(row):
    sentence1 = row['sentence1']
    sentence2 = row['sentence2']
    prompt = """Ниже приведена инструкция, описывающая задачу, сопровождаемая вводом, который предоставляет дополнительный контекст. Напишите ответ, который соответствующим образом завершает запрос."""
    instruction = """Данное предложение, ваша задача - сгенерировать его отрицание в формате json"""
    input = str(sentence1)
    response = f"""```json
{{'orignal_sentence': '{sentence1}', 'generated_negation': '{sentence2}'}}
```
"""
    if len(input.strip()) == 0:  #  prompt + 2 new lines + ###instruction + new line + input + new line + ###response
        text = prompt + "\n\n### Инструкция:\n" + instruction + "\n### Ответ:\n" + response
    else:
        text = prompt + "\n\n### Инструкция:\n" + instruction + "\n### Ввод:\n" + input + "\n" + "\n### Ответ:\n" + response
    
    # we need 4 columns for auto train, instruction, input, output, text
    return pd.Series([instruction, input, response, text])

new_df = df[df['gold_label'] == 'contradiction'][['sentence1', 'sentence2']].apply(convert_to_format, axis=1)
new_df.columns = ['instruction', 'input', 'output', 'text']

new_df.to_csv('snli_instruct.csv', index=False)

 

Вот пример образца данных:

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

### Инструкция:
Учитывая следующее предложение, ваша задача - сгенерировать его отрицание в формате json
### Входные данные:
Пара играющая с маленьким мальчиком на пляже.

### Ответ:
```json
{'orignal_sentence': 'Пара играющая с маленьким мальчиком на пляже.', 'generated_negation': 'Пара наблюдает, как маленькая девочка играет сама на пляже.'}
```

 

Теперь у нас есть наш набор данных в правильном формате, давайте начнем с настройки. Прежде чем начать, установим необходимые пакеты. Мы будем использовать accelerate, peft (Parameter efficient Fine Tuning), в сочетании с Hugging Face Bits and bytes и transformers.

!pip install -q accelerate==0.21.0 peft==0.4.0 bitsandbytes==0.40.2 transformers==4.31.0 trl==0.4.7

 

import os
import torch
from datasets import load_dataset
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
    HfArgumentParser,
    TrainingArguments,
    pipeline,
    logging,
)
from peft import LoraConfig, PeftModel
from trl import SFTTrainer

 

Вы можете загрузить отформатированный набор данных на диск и загрузить его в Colab.

from google.colab import drive
import pandas as pd

drive.mount('/content/drive')

df = pd.read_csv('/content/drive/MyDrive/snli_instruct.csv')

 

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

from datasets import Dataset

dataset = Dataset.from_pandas(df)

 

Мы будем использовать уже квантованную модель LLamav2, которая предоставляется abhishek/llama-2–7b-hf-small-shards. Давайте определим некоторые гиперпараметры и переменные здесь:

# Модель, которую вы хотите обучить из Hugging Face hub
model_name = "abhishek/llama-2-7b-hf-small-shards"

# Имя модели после донастройки
new_model = "llama-2-contradictor"

################################################################################
# Параметры QLoRA
################################################################################

# Размерность внимания LoRA
lora_r = 64

# Параметр Alpha для масштабирования LoRA
lora_alpha = 16

# Вероятность отсева для слоев LoRA
lora_dropout = 0.1

################################################################################
# Параметры bitsandbytes
################################################################################

# Активация 4-битной точности при загрузке базовой модели
use_4bit = True

# Вычислительный тип для 4-битных базовых моделей
bnb_4bit_compute_dtype = "float16"

# Тип квантования (fp4 или nf4)
bnb_4bit_quant_type = "nf4"

# Активация вложенного квантования для 4-битных базовых моделей (двойное квантование)
use_nested_quant = False

################################################################################
# Параметры TrainingArguments
################################################################################

# Каталог вывода, где будут храниться предсказания модели и контрольные точки
output_dir = "./results"

# Количество эпох обучения
num_train_epochs = 1

# Включение обучения fp16/bf16 (установите bf16 в True с использованием A100)
fp16 = False
bf16 = False

# Размер пакета на каждый GPU для обучения
per_device_train_batch_size = 4

# Размер пакета на каждый GPU для оценки
per_device_eval_batch_size = 4

# Количество шагов обновления для накопления градиентов
gradient_accumulation_steps = 1

# Включение чекпоинтинга градиентов
gradient_checkpointing = True

# Максимальная норма градиента (обрезка градиента)
max_grad_norm = 0.3

# Начальная скорость обучения (оптимизатор AdamW)
learning_rate = 1e-5

# Весовое убывание для всех слоев, кроме смещения/нормализации слоев
weight_decay = 0.001

# Оптимизатор для использования
optim = "paged_adamw_32bit"

# Тип расписания скорости обучения
lr_scheduler_type = "cosine"

# Количество шагов обучения (переопределяет num_train_epochs)
max_steps = -1

# Соотношение шагов для линейного разогрева (от 0 до скорости обучения)
warmup_ratio = 0.03

# Группировка последовательностей в пакеты с одинаковой длиной
# Экономит память и значительно ускоряет обучение
group_by_length = True

# Сохранение контрольной точки каждые X шагов обновления
save_steps = 0

# Журналирование каждые X шагов обновления
logging_steps = 100

################################################################################
# Параметры SFT
################################################################################

# Максимальная длина последовательности для использования
max_seq_length = None

# Упаковка нескольких коротких примеров в одну входную последовательность для повышения эффективности
packing = False

# Загрузка всей модели на GPU 0
device_map = {"": 0}

 

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

Теперь мы можем просто использовать класс BitsAndBytesConfig для создания конфигурации для 4-битного тонкой настройки.

compute_dtype = getattr(torch, bnb_4bit_compute_dtype)

bnb_config = BitsAndBytesConfig(
    load_in_4bit=use_4bit,
    bnb_4bit_quant_type=bnb_4bit_quant_type,
    bnb_4bit_compute_dtype=compute_dtype,
    bnb_4bit_use_double_quant=use_nested_quant,
)

 

Теперь мы можем загрузить базовую модель с использованием 4-битной конфигурации BitsAndBytesConfig и токенизатора для тонкой настройки.

tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"

model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=bnb_config,
    device_map=device_map
)
model.config.use_cache = False
model.config.pretraining_tp = 1

 

Теперь мы можем создать конфигурацию LoRA и установить параметры обучения.

# Загрузка конфигурации LoRA
peft_config = LoraConfig(
    lora_alpha=lora_alpha,
    lora_dropout=lora_dropout,
    r=lora_r,
    bias="none",
    task_type="CAUSAL_LM",
)

# Установка параметров обучения
training_arguments = TrainingArguments(
    output_dir=output_dir,
    num_train_epochs=num_train_epochs,
    per_device_train_batch_size=per_device_train_batch_size,
    gradient_accumulation_steps=gradient_accumulation_steps,
    optim=optim,
    save_steps=save_steps,
    logging_steps=logging_steps,
    learning_rate=learning_rate,
    weight_decay=weight_decay,
    fp16=fp16,
    bf16=bf16,
    max_grad_norm=max_grad_norm,
    max_steps=max_steps,
    warmup_ratio=warmup_ratio,
    group_by_length=group_by_length,
    lr_scheduler_type=lr_scheduler_type,
    report_to="tensorboard"
)

 

Теперь мы можем просто использовать SFTTrainer, который предоставляется trl от HuggingFace, чтобы начать обучение.

# Установка параметров надзорного обучения
trainer = SFTTrainer(
    model=model,
    train_dataset=dataset,
    peft_config=peft_config,
    dataset_text_field="text",  # это столбец с текстом в наборе данных
    max_seq_length=max_seq_length,
    tokenizer=tokenizer,
    args=training_arguments,
    packing=packing,
)

# Обучение модели
trainer.train()

# Сохранение обученной модели
trainer.model.save_pretrained(new_model)

 

Это начнет обучение на заданное количество эпох. После обучения модели обязательно сохраните ее на диск, чтобы вы могли загрузить ее снова (так как вам придется перезапустить сеанс в Colab). Вы можете сохранить модель на диске с помощью команды zip и mv.

!zip -r llama-contradictor.zip results llama-contradictor
!mv llama-contradictor.zip /content/drive/MyDrive

 

Теперь, когда вы перезапускаете сеанс Colab, вы можете вернуть его в свою сессию снова.

!unzip /content/drive/MyDrive/llama-contradictor.zip -d .

 

Вам необходимо снова загрузить базовую модель и объединить ее с настроенными матрицами LoRA. Это можно сделать с помощью функции merge_and_unload().

tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"

base_model = AutoModelForCausalLM.from_pretrained(
    "abhishek/llama-2-7b-hf-small-shards",
    low_cpu_mem_usage=True,
    return_dict=True,
    torch_dtype=torch.float16,
    device_map={"": 0},
)

model = PeftModel.from_pretrained(base_model, '/content/llama-contradictor')
model = model.merge_and_unload()
pipe = pipeline(task="text-generation", model=model, tokenizer=tokenizer, max_length=200)

 

Вывод

 

Вы можете протестировать свою модель, просто передавая входные данные в том же шаблоне запроса, который мы определили выше.

prompt_template = """### Инструкция:
Учитывая следующее предложение, ваша задача состоит в том, чтобы сгенерировать его отрицание в формате json
### Ввод:
{}

### Ответ:
"""

sentence = "Прогноз погоды предсказывает солнечный день с высокой температурой около 30 градусов Цельсия, идеально подходящий для дня на пляже с друзьями и семьей."

input_sentence = prompt_template.format(sentence.strip())

result = pipe(input_sentence)
print(result)

 

Результат

 

### Инструкция:
Учитывая следующее предложение, ваша задача состоит в том, чтобы сгенерировать его отрицание в формате json
### Ввод:
Прогноз погоды предсказывает солнечный день с высокой температурой около 30 градусов Цельсия, идеально подходящий для дня на пляже с друзьями и семьей.

### Ответ:
```json
{
  "sentence": "Прогноз погоды предсказывает солнечный день с высокой температурой около 30 градусов Цельсия, идеально подходящий для дня на пляже с друзьями и семьей.",
  "negation": "Прогноз погоды предсказывает дождливый день с низкой температурой около 10 градусов Цельсия, не идеально подходящий для дня на пляже с друзьями и семьей."
}
```

 

Фильтрация полезного вывода

 

Бывает, что модель продолжает прогнозировать даже после генерации ответа из-за ограничения на количество токенов. В этом случае вам нужно добавить функцию постобработки, которая фильтрует часть JSON, которая нам нужна. Это можно сделать с помощью простого регулярного выражения.

import re
import json

def format_results(s):
  pattern = r'```json\n(.*?)\n```'

  # Найти все вхождения JSON-объектов в строке
  json_matches = re.findall(pattern, s, re.DOTALL)
  if not json_matches:
    # попробуйте найти 2-ой шаблон
    pattern = r'\{.*?"sentence":.*?"negation":.*?\}'
    json_matches = re.findall(pattern, s)

  # Вернуть первый найденный JSON-объект или None, если совпадение не найдено
  return json.loads(json_matches[0]) if json_matches else None

 

Это даст вам требуемый вывод вместо того, чтобы модель повторяла случайные выходные токены.

 

Резюме

 

В этой статье вы узнали основы QLora, настройку модели LLama v2 на Colab с использованием QLora, настройку инструкций и пример шаблона из набора данных Alpaca, который может быть использован для дальнейшей настройки модели.

 

Ссылки

 

[1]: QLoRA: Efficient Finetuning of Quantized LLMs, 23 мая 2023 г., Тим Деттмерс и др.

[2]: https://huggingface.co/blog/hf-bitsandbytes-integration

[3]: https://huggingface.co/blog/4bit-transformers-bitsandbytes

[4]: https://www.youtube.com/watch?v=y9PHWGOa8HA

[5]: https://arxiv.org/abs/2106.09685

[6]: https://aclanthology.org/2022.acl-long.244/

[7]: https://crfm.stanford.edu/2023/03/13/alpaca.html

[8]: Блокнот Colab от @maximelabonne https://colab.research.google.com/drive/1PEQyJO1-f6j0S_XJ8DV50NkpzasXkrzd?usp=sharing

    Ахмад Анис является энтузиастом машинного обучения и исследователем, в настоящее время работает в redbuffer.ai. Помимо своей основной работы, Ахмад активно взаимодействует с сообществом машинного обучения. Он является региональным лидером Cohere for AI, некоммерческой организации, посвященной открытой науке, и является членом сообщества AWS. Ахмад активно вносит свой вклад в Stackoverflow, где у него более 2300 баллов. Он внес свой вклад во многие известные проекты с открытым исходным кодом, включая Shap-E от OpenAI.