с Адитья Хандекар CorridorPlatforms

Большие языковые модели (LLM) стали важной силой в семействе генеративного ИИ, захватывая воображение огромными возможностями. С запуском ChatGPT организации и компании все чаще используют эти сложные модели в существующих аналитических конвейерах, а также порождают новые варианты использования, которые были невозможны до LLM. Однако, хотя привлекательность генеративного ИИ может быть захватывающей, важно понимать, что развертывание LLM в производственной среде может нести значительные риски и последствия, если модели не работают должным образом. На это есть несколько причин… и в этой серии блогов, состоящей из трех частей, мы углубимся в наш опыт тонкой настройки и использования LLM для конкретного случая использования. Наше путешествие будет включать в себя определение постановки проблемы, определение аналитического конвейера и получение ключевых выводов из наших экспериментов для оценки производительности модели и устранения рисков галлюцинаций. Мы также поделимся лучшими практиками.

Сериал из трех частей строится так

  1. Определение постановки задачи и нашего аналитического конвейера.
  2. Основные выводы и выводы: работа с моделями и риски галлюцинаций.
  3. Чем производительность LLM, использующего только декодер, отличается от LLM, использующих только кодировщик (BERT) и LLM на основе кодера-декодера (Flan T5) для той же задачи?

Определение нашей постановки задачи и набора данных

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

  1. кредитная отчетность
  2. взыскание долгов
  3. ипотеки и кредиты
  4. кредитные карты
  5. розничные банковские услуги

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

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

Правильный выбор модели фундамента

Базовые модели (в контексте языкового моделирования) определяются как предварительно обученные большие языковые модели, обученные на огромных объемах данных для выполнения нескольких задач. Он представляет собой отправную точку современного языкового моделирования. Отсюда мы можем точно настроить модель, добавить подсказки, добавить базу знаний и многое другое, но все начинается с базовой модели. Обратите внимание, что не все FM созданы равными. Некоторые из них могут быть специфичными для домена (например, BloombergGPT от Bloomberg). Даже модели Фонда, обученные на одинаковых наборах данных, также могут различаться по своим ответам. Вот некоторые примеры:

  1. Ответ от Llama 7B будет отличаться от чата Llama 7B (инструкция настроена)
  2. Реакция Falcon 7B и Llama 7B может отличаться в разных сценариях.
  3. Ответ Falcon будет полностью отличаться от ответов BloombergGPT.

В нашем случае важно было выбрать модель с лучшими возможностями понимания естественного языка и размером. По мере того, как на рынке создается больше LLM, широкая диаспора из них будет доступна для вертикали, выбор правильного ОЧЕНЬ важен для успеха варианта использования. Когда мы начинали, Falcon был довольно популярен. Поэтому мы выбрали Falcon 7B для процесса тонкой настройки. Falcon — это семейство LLM, созданное Институтом технологических инноваций. Эти модели были аналогичны моделям LLaMA при сравнении со стандартными тестами.

Почему бы просто не использовать подсказки и LangChain для процесса?

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

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

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

Тонкая настройка LLM

Для общей задачи классификации текста популярной архитектурой для использования является модель текстового кодировщика на основе Transformer для получения вектора встраивания, за которым следует нейронная сеть (необязательно) и, наконец, контролируемый классификатор для классификации вывода. На изображении ниже показано, как мы можем точно настроить модель BERT (модель только для кодировщика) для задачи классификации текста.

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

Но в случае LLM такая же методология тонкой настройки, как BERT, может не работать по двум основным причинам.

  1. Количество параметров для модели фундамента может быть очень большим (такие модели, как LLaMA или Falcon, имеют параметры модели в диапазоне от 7B до 70B). Следовательно, их точная настройка с помощью традиционных подходов к трансфертному обучению может потребовать очень больших вычислительных ресурсов.
  2. Вы также рискуете «катастрофически забыть», когда базовая модель теряет базовую память при переобучении.

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

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

Чтобы получить лучшее представление, представьте, что у вас есть весовая матрица W размерностью 768 x 768. Теперь мы можем разложить матрицу на две матрицы W_A и W_B так, чтобы W_A (768 x r) и W_B (r x 768). Теперь мы можем определить нашу матрицу W как W = W_A @ W_B (где @ — умножение матриц). Таким образом, первоначально количество обучаемых параметров W было 768 * 768 = 589824, тогда как теперь общее количество обучаемых параметров W как разложение W_A и W_B становится 768 × 8) + (8 × 768) = 12288, что является уменьшением параметров на 97 %. Вот псевдокод, чтобы понять это больше

# inspired from the blog by Sebastian Raschka
# https://sebastianraschka.com/blog/2023/llm-finetuning-lora.html

import torch 
import torch.nn as nn

# define the input and the output dimension of the neural network 
# to define the size of the weight metrix
# let's assume the size of our weight matrix becomes W of dimension (768 x 768)
# the total number of parameters in W is 768 * 768 = 589824

input_dim = 768 
output_dim = 768 
W = ... # weight of my neural network

# the rank 'r' is for the low rank adaptation
# we can represent our weight W as product of two matrix W_A and W_B such that
# W (768 x 768) = W_A (768 x r) @ (r x 768) 
# the total number of parameters we have now is (768 × 8) + (8 × 768) = 12288

# hence we define our W_A and W_B with r = 8
rank = 8 
W_A = nn.Parameter(torch.empty(input_dim, rank)) # LoRA weight A
W_B = nn.Parameter(torch.empty(rank, output_dim)) # LoRA weight B

# this is how my regular feed neural net model with weights W look like

def regular_forward_matmul(x, W):
    h = x @ W
    return h

# and this is how my feed forward with LoRA looks like

def lora_forward_matmul(x, W, W_A, W_B):
    # regular matrix multiplication 
    # where W is NOT trainable (froozen weights)
    h = x @ W  
    h += x @ (W_A @ W_B) * alpha # use scaled LoRA weights
    return h

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

В то время как LoRA (адаптеры низкого ранга) — это эффективный метод, который сокращает использование памяти во время тонкой настройки, QLoRA развивает этот подход еще дальше. QLoRA вводит 4-битное квантование для сжатия параметров предварительно обученной языковой модели, что позволяет выполнять точную настройку на небольших графических процессорах. Используя комбинацию 4-битного NormalFloat (NF4) и страничных оптимизаторов, QLoRA обеспечивает экономию памяти без особого ущерба для производительности. Это новшество позволяет QLoRA превосходить предыдущие модели в тесте Vicuna и настраивать большие модели на потребительском оборудовании с замечательной эффективностью. Чтобы узнать больше о QLoRA, ознакомьтесь с замечательным постом в блоге Hugging Face.

Наш общий аналитический конвейер с весами и смещением

Вернемся к нашему подходу к построению аналитического конвейера для классификации текста. Мы начали с тонкой настройки модели Falcon 7B. Мы использовали Hugging Face для использования и тонкой настройки наших LLM. Мы начали с загрузки и построения нашего набора данных. На рис. 1 показано, как выглядит наш набор данных. В нем есть столбец с именем narrative (который содержал наш текст) и еще один столбец с именем product, который содержал нашу метку.

Построение набора данных

Поскольку мы имеем дело с языковой моделью в стиле декодера, наш набор данных не должен выглядеть как традиционный контролируемый набор данных (содержащий функции и метки). Мы имитируем задачу классификации как задачу завершения языка. Интуитивно понятное языковое моделирование — это просто предсказание следующего токена по предыдущему. Следовательно, наша подсказка была построена таким образом, что она предсказывает следующий токен, учитывая всю жалобу клиента. Где ожидаемый следующий набор токенов будет нашими метками классов. Вот как выглядело приглашение.

Здесь текст, отмеченный зеленым цветом, является нашим фактическим входным текстом, желтый — нашим начальным текстом, а начиная с ### Assistant — это то, что, как ожидается, будет предсказывать наша языковая модель. Такие маркеры, как ###System, ###Assistant, <|system prompt>| и т. д., используются во время обучения или тонкой настройки языковых моделей, чтобы направлять поведение модели и генерировать соответствующие ответы в определенных контекстах. Эти маркеры действуют как инструкции или подсказки для модели, чтобы следовать определенному формату или обеспечивать определенные типы ответов, когда она встречает их во входном тексте.

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

Ниже приведен пример кода, который показывает, как мы можем преобразовать текст клиентов в требуемый формат. Вы можете найти весь наш код, используемый в нашем пайплайне, в этом репозитории GitHub.

def format_text(
    self,
    row,
    additional_prompt: Optional[str] = None,
    test: Optional[bool] = None,
) -> str:

    feature_row = row[self.feature]
    label_row = row[self.label]
    additional_prompt = "" if additional_prompt is None else additional_prompt + "\n"

    if not test:
        format_text = (
            additional_prompt
            + f"### Human: This consumer complaint: {feature_row} is classified into category: "
            + f"### Assistant: {label_row}"
        )
    else:
        format_text = (
            additional_prompt
            + f"### Human: This consumer complaint: {feature_row} is classified into category:"
        )
    return format_text

После форматирования мы сохранили наш набор данных в виде JSON, где каждый большой двоичный объект в JSON был таким:

# if train
{
    'text': '### Human .... ### Assistant ...'
}

# validation and test
{
    'text': '### Human ....',
    'label': '<label>'
}

После сохранения мы использовали библиотеку Hugging Face datasets для загрузки этих JSON в формат набора данных Hugging Face.

Конвейер тонкой настройки

После завершения процесса создания набора данных мы загрузили нашу модель с помощью библиотеки Hugging Face transformers. Поскольку эти модели являются каузальными языковыми моделями, мы использовали AutoModelForCausalLM для загрузки модели в 4-битном квантовании. Вся часть квантования обрабатывается BitsAndBytes библиотекой. Вот пример кода.

from transformers import (AutoModelForCausalLM, AutoTokenizer,
                          BitsAndBytesConfig, TrainingArguments)


model_id =  "tiiuae/falcon-7b"

# load the essential configurations for quantization
quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
)

# load the model

model = AutoModelForCausalLM.from_pretrained(
    model_id,
    device_map="auto",
    quantization_config=quantization_config,
    trust_remote_code=True,
)

Чтобы упростить выполнение и гибкость наших экспериментов, в качестве передовой практики мы структурировали наш конвейер в параметризованные функции кода с использованием классов Python. Мы абстрагировались от тонкой настройки и конвейера вывода обнимающих лиц с помощью нашего класса с именами SimpleFineTuner и SimpleInference, которые соответствовали нашим потребностям в конкретной задаче.

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

И после этого наш общий код сжался в несколько показанных строк.

# load all the libraries

import os
import json
import wandb
import warnings
import pandas as pd

from src.finetune import SimpleFineTuner
from src.inference import SimpleInference
from src.dataset import FalconFineTuningDataset


# load the csv 

PATH = os.path.join(os.getcwd(), os.pardir, os.pardir, 'Data')
csv_path = os.path.join(PATH, 'complaints_processed.csv')

# Preprocess the data and create the dataset

test_size = 150
data = pd.read_csv(csv_path)
data['product'] = data['product'].str.replace('_', ' ')
train_df, test_df = data.iloc[:-test_size], data.iloc[-test_size:]

dataset = FalconFineTuningDataset(
    train_df= train_df,
    test_df = test_df,
    feature = 'narrative',
    label = 'product'
).get_hf_dataset(
    format_style=1, 
    json_folder_path=PATH, 
    validation_size=10000, 
    shuffle=True, 
    seed=42
)

# get the train, validation and test dataset

train_dataset = dataset['train_dataset']
eval_dataset  = dataset['eval_dataset']
test_dataset  = dataset['test_dataset']

# start writing all the necessary configurations
# for the model and load the model

model_id =  "tiiuae/falcon-7b"

# load the PEFT (Parameter Efficient Fine Tuning) config
# this includes what will the size of r for our LoRA weights
# scalar value alpha, and where to apply LoRA (target modules)

peft_config_dict = {
    'r': 64,
    'lora_alpha' : 16,
    'lora_dropout' : 0.1,
    
    'bias': 'none',
    'task_type': 'CAUSAL_LM',
    'target_modules': [
        "query_key_value", 
        "dense", 
        "dense_h_to_4h", 
        "dense_4h_to_h"
    ],
}

# define the training arguments as json 
# tip: set model's output directory inside where all the code is

train_args = {
    "output_dir": "./falcon_7b_output",
    "per_device_train_batch_size": 4,
    "gradient_accumulation_steps": 4,
    "optim": "paged_adamw_32bit",
    "save_steps": 10,
    "save_total_limit":5,
    "logging_steps": 10,
    "learning_rate": 2e-4,
    "max_grad_norm": 0.3,
    "max_steps": 250, # epochs 
    "warmup_ratio": 0.03,
    "lr_scheduler_type": "constant",
    "report_to": "wandb",
    'run_name': "test-run-falcon7b-pretrained"
    
}

# make the dataset config dict

dataset_config_dict = {
    'train_dataset': train_dataset,
    'eval_dataset' : eval_dataset,
    'dataset_text_field': 'text',
    'max_seq_length': 512,
}


# instantiate our fine tuning pipeline 
finetuner = SimpleFineTuner(wandb_project_name, model_id)
model, tokenizer = finetuner.load_base_model()
tokenizer.pad_token = tokenizer.eos_token

# instantiate the hugging face trainer. 
# we used TRL (Transformer Reinforcement Learning) which is a wrapper around
# hugging face Trainer class. (We did not used Reinforcement Learning)
# for fine tuning, just supervised fine tuning
# more on TRL: https://huggingface.co/docs/trl/index

trainer = finetuner.load_trl_trainer(
    model, 
    tokenizer, peft_config_dict, train_args, dataset_config_dict
)

# train the model
trainer.train()

# evaluate the model
trainer.evaluate()

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

Пайплайн проверки и тестирования

Для любой организации, включающей LLM в свои системы, очень важно оценивать модели по этим трем основным параметрам (их может быть больше):

  1. Производительность модели (например, насколько хороши сгенерированные ответы)
  2. Производительность модели по скорости
  3. Тестирование модели на надежность и устойчивость.

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

  1. Сгенерированный текст не будет содержать ожидаемых текстовых меток и будет содержать только случайные тексты (большая дисперсия).
  2. Сгенерированный текст будет содержать только ожидаемые метки (без отклонений)
  3. Сгенерированный текст будет содержать наши ожидаемые метки вместе с некоторым расширенным текстом (умеренная дисперсия).

Поскольку мы не можем определить жесткую и быструю метрику на основе правил, такую ​​как точность оценки LLM для классификации, мы придумали метрику под названием неопределенная точность. Алгоритм прост, сгенерированный текст и метки одинаковы, если метка содержится внутри сгенерированного текста. Соответственно, мы попытались количественно оценить производительность нашей модели. Производительность нашей исходной модели составила 66 % в нашем тестовом наборе. Тем не менее, мы значительно улучшили его с помощью большего количества итераций. Подробнее в нашем втором блоге, так что следите за обновлениями.

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

После завершения процесса тонкой настройки с 4-битным квантованием и сохранения всех весов PEFT я заметил проблему при попытке загрузить веса нашей модели в Hugging Face Hub, поскольку эта функция еще не была реализована. Кроме того, вставка каталога веса модели (falcon_7b_output) приведет к ошибке. Чтобы решить эту проблему, убедитесь, что вы находитесь вне папки, содержащей точно настроенные веса PEFT. Предполагая, что вы выполнили один и тот же код, вы найдете несколько контрольных точек модели в папке весов. Ниже приведен пример структуры папок.

falcon_7b_output/
├── checkpoint-48
│   ├── README.md
│   ├── adapter_config.json
│   ├── adapter_model.bin
├── checkpoint-52
│   ├── README.md
│   ├── adapter_config.json
│   ├── 

Помните, как мы определяем идентификатор модели в Hugging Face, tiiuae/falcon-7b , поэтому в первый раз Hugging Face загружает его из своего репозитория моделей, а в следующий раз он загружается внутри .cache/hugging-face вашего локального. Следовательно, формат загрузки весов PEFT в этом случае будет falcon_7b_output/checkpoint-48. Hugging Face теперь будет смотреть, присутствует ли там относительный каталог или нет. И, следовательно, загрузит модель успешно.

Вот как выглядит код вывода для этого.

model_id =  "tiiuae/falcon-7b"
adapter_id = 'falcon_7b_output/checkpoint-48'

quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
)

# load the actual foundation model

model = AutoModelForCausalLM.from_pretrained(
    model_id,
    return_dict=True,
    device_map='auto',
    trust_remote_code = True,
    output_attentions=True,
    quantization_config = quantization_config # may be this can be an arg
)

# load the peft weights and attach it to the base

model = PeftModel.from_pretrained(model, adapter_id)
tokenizer = AutoTokenizer.from_pretrained(model_id)

И теперь вы можете сделать вывод или проверить свою точно настроенную модель.

Организация конвейера экспериментов с помощью Weights and Bias

У нас было много экспериментов. И управлять экспериментами во множестве ноутбуков было трудно. Были разные варианты моделей, наборов данных и даже подсказок. И, следовательно, писать один и тот же код, но с небольшими изменениями для всех этих вариаций и отслеживать их, было сложно. И поэтому мы решили интегрировать Weights и Bias в наш конвейер.

Для тех, кто не знаком с Weights and Bias, Weights and Bias (W&B) — это платформа машинного обучения, которая предоставляет инструменты для отслеживания экспериментов, визуализации моделей и совместной работы, позволяя ученым и исследователям данных лучше понимать и оптимизировать свои модели. производительность за счет простых в использовании интерфейсов и интеграций.

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

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

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

Заключение

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

Рекомендации