Преобразование текста в векторы надстройка вставок с помощью неподконтрольного подхода TSDAE

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

Создано Freepik

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

Введение

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

В большинстве случаев предварительно обученный предложный преобразователь будет работать «из коробки» и предоставит разумные результаты. Существует много выборов предварительно обученных контекстных вложений на основе BERT, некоторые специализированные для определенной области, которые можно использовать в таких случаях, и они доступны для загрузки с платформ, таких как HuggingFace.

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

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

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

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

Мотивация

Эта работа была вдохновлена недавним исследованием [aviation_article], которое фокусируется на авиационной области, у которого есть уникальные характеристики, такие как техническая жаргон, сокращения и нестандартная грамматика.

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

План

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

Изображение от автора

Во-первых, я начинаю с предварительного обучения, сосредоточенного на целевом домене, часто называемого адаптивным предварительным обучением. В этой фазе требуется набор предложений из нашего набора данных. Для этой стадии я использую метод TSDAE, который отлично справляется с адаптацией домена как задачей предварительного обучения и значительно превосходит другие методы, включая модель языка с маскировкой, как подчеркивается в статье [tsdae_article]. Я тесно следую скрипту: train_tsdae_from_file.py.

Затем я провожу донастройку модели на общей размеченной выборке данных AllNLI, используя стратегию множественной потери отрицательного ранжирования. Для этой стадии я использую скрипт из training_nli_v2.py. Как документировано в статье [tsdae_article], этот дополнительный шаг не только противодействует переобучению, но также существенно улучшает производительность модели.

TSDAE — Предварительное обучение на целевой области

TSDAE (трансформерный последовательный детектор-кодировщик) — это необучаемый метод встраивания предложений, который впервые был представлен K. Wang, N. Reimers и I. Gurevych в [tsdae_article].

TSDAE использует модифицированный дизайн трансформера кодировщик-декодировщик, где ключ и значение взаимного внимания ограничены встраиванию предложения. Я расскажу подробности в контексте оптимальных выборов архитектуры, выделенных в оригинальной статье [tsdae_article].

Изображение автора
  • Набор данных состоит из неразмеченных предложений, которые в процессе предварительной обработки подвергаются повреждению путем удаления 60% их содержимого для введения шума ввода.
  • Кодировщик преобразует поврежденные предложения в векторы фиксированного размера путем пулинга их вложенных слов. Согласно статье [tsdae_article], для извлечения вектора предложения рекомендуется использовать метод пулинга CLS.
  • Декодировщик должен восстановить исходное входное предложение из поврежденного встраивания предложения. Авторы советуют привязывать параметры кодировщика и декодировщика во время обучения для сокращения количества параметров в модели, что облегчает обучение и уменьшает вероятность переобучения, не влияя на производительность.

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

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

Во время вывода используется только кодировщик для создания встраивания предложений.

Модель обучается восстанавливать чистое предложение из поврежденного предложения, и это достигается путем максимизации целевой функции:

Изображение автора

AllNLI — Набор данных для естественной языковой инференции

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

В этом эксперименте я использую набор данных AllNLI, который содержит более 900 тысяч записей, объединение наборов данных Stanford Natural Language Inference (SNLI) и MultiNLI. Этот набор данных можно скачать по ссылке: AllNLI download site.

Загрузка и подготовка данных для предварительного обучения

Для создания нашего специализированного набора данных мы используем набор данных Kaggle arXiv, состоящий примерно из 1,7 миллиона научных работ по STEM-направлениям, полученных из известной электронной предварительной платформы arXiv. Кроме заголовка, аннотации и авторов, каждой статье сопровождается значительное количество метаданных. Однако здесь нас интересуют только заголовки.

После скачивания я выберу предварительные расчеты по математике. Учитывая размер файла Kaggle, я добавил уменьшенную версию файла математических статей на Github для удобного доступа. Однако, если вас интересует другая тема, скачайте набор данных и замените в коде math на желаемую тему:

# Сбор статей по теме "math"def extract_entries_with_math(filename: str) -> List[str]:    """    Функция для извлечения записей, содержащих строку 'math' в поле 'id'.    """    # Инициализация пустого списка для хранения извлеченных записей.    entries_with_math = []    with open(filename, 'r') as f:        for line in f:            try:                # Загрузка объекта JSON из строки                data = json.loads(line)                # Проверка наличия ключа "id" и наличия в нем "math"                if "id" in data and "math" in data["id"]:                    entries_with_math.append(data)            except json.JSONDecodeError:                # Вывод сообщения об ошибке, если строка не является допустимым JSON                print(f"Невозможно прочитать: {line}")    return entries_with_math# Извлечение математических статейentries = extract_entries_with_math(arxiv_full_dataset)# Сохранение набора данных как объекта JSONarxiv_dataset_math = file_path + "/data/arxiv_math_dataset.json"with open(arxiv_dataset_math, 'w') as fout:    json.dump(entries, fout)

Я загрузил наш набор данных в объект-кадр Pandas df. Быстрый осмотр показывает, что уменьшенный набор содержит 55 497 предварительных сообщений — более практический размер для нашего эксперимента. В то время как [tsdae_article] предлагает, что достаточно около 10 тысяч записей, я сохраню весь уменьшенный набор данных. Заголовки математических статей могут содержать код LaTeX, который я заменю на ISO-код для оптимизации обработки.

parsed_titles = []for i,a in df.iterrows():    """    Функция для замены кода LaTeX на ISO-код.    """    try:        parsed_titles.append(LatexNodes2Text().latex_to_text(a['title']).replace('\\n', ' ').strip())     except:        parsed_titles.append(a['title'].replace('\\n', ' ').strip())# Создаем новый столбец с обработанными заголовкамиdf['parsed_title'] = parsed_titles

Я буду использовать записи parsed_title для обучения, поэтому давайте извлечем их в виде списка:

# Извлекаем обработанные заголовки в виде спискatrain_sentences = df.parsed_title.to_list()

Затем сформируем исказленные предложения, удалив примерно 60% токенов из каждой записи. Если вас интересует дальнейшее исследование или попытка других коэффициентов удаления, ознакомьтесь со скриптом для удаления шума.

# Добавляем шум к даннымtrain_dataset = datasets.DenoisingAutoEncoderDataset(train_sentences)

Давайте посмотрим, что произошло с одной записью после обработки:

print(train_dataset[2010])
начальный текст: "Решения уравнений Бете для модели XXZ"
искаженный текст: "Решения для модели XXZ"

Как вы видите, из начального текста были удалены уравнения Бете и модель.

Последний шаг в обработке наших данных — это загрузка набора данных по партиям:

train_dataloader = DataLoader(train_dataset, batch_size=8,                               shuffle=True, drop_last=True)

Обучение TSDAE

Хотя я буду следовать подходу из файла rain_tsdae_from_file.py, я структурирую его пошагово для лучшего понимания.

Начнем с выбора предварительно обученной модели трансформера и оставим параметры по умолчанию:

model_name = 'bert-base-uncased'word_embedding_model = models.Transformer(model_name)

Выберем метод пулинга CLS и укажем размерность векторов, которые будут созданы:

pooling_model = models.Pooling(word_embedding_model.get_word_embedding_dimension(),                               "cls")                                            'cls')

Затем построим модель предложений, объединив два слоя:

model = SentenceTransformer(modules=[word_embedding_model,                            pooling_model])                                                  pooling_model])

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

train_loss = losses.DenoisingAutoEncoderLoss(model,                                             decoder_name_or_path=model_name,                                             tie_encoder_decoder=True)

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

model.fit(    train_objectives=[(train_dataloader, train_loss)],    epochs=1,    weight_decay=0,    scheduler='constantlr',    optimizer_params={'lr': 3e-5},    show_progress_bar=True,    use_amp=True # установите False, если GPU не поддерживает ядра FP16)pretrained_model_save_path = 'output/tsdae-bert-uncased-math'model.save(pretrained_model_save_path)

Этап предварительного обучения занял примерно 15 минут на Google Colab Pro с использованием A100 GPU на High-RAM.

Донастройка на наборе данных AllNLI

Давайте начнем скачивание набора данных AllNLI:

nli_dataset_path = 'data/AllNLI.tsv.gz'if not os.path.exists(nli_dataset_path):    util.http_get('<https://sbert.net/datasets/AllNLI.tsv.gz>',                   nli_dataset_path)

<!–Далее, распакуйте файл и разберите данные для обучения:

def add_to_samples(sent1, sent2, label):    if sent1 not in train_data:        train_data[sent1] = {'contradiction': set(),                             'entailment': set(),                              'neutral': set()}                                                       'entailment': set                                               'neutral': set()}    train_data[sent1][label].add(sent2)train_data = {}with gzip.open(nli_dataset_path, 'rt', encoding='utf8') as fIn:    reader = csv.DictReader(fIn, delimiter='\\t',                             quoting=csv.QUOTE_NONE)    for row in reader:        if row['split'] == 'train':            sent1 = row['sentence1'].strip()            sent2 = row['sentence2'].strip()                        add_to_samples(sent1, sent2, row['label'])            add_to_samples(sent2, sent1, row['label'])  # Also add the oppositetrain_samples = []for sent1, others in train_data.items():    if len(others['entailment']) > 0 and len(others['contradiction']) > 0:        train_samples.append(InputExample(texts=[sent1,                      random.choice(list(others['entailment'])),                      random.choice(list(others['contradiction']))]))        train_samples.append(InputExample(texts=[random.choice(list(others['entailment'])),                      sent1,                      random.choice(list(others['contradiction']))]))                                                            random.choice(list(others['contradiction']))]))

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

train_dataloader = datasets.NoDuplicatesDataLoader(train_samples,                                                   batch_size=32)

Размер пакета, который я использую здесь, меньше размера по умолчанию 128 из скрипта. Хотя больший пакет дает лучшие результаты, он требует больше памяти GPU, и так как я ограничен вычислительными ресурсами, я выбираю меньший размер пакета.

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

# Установите параметры моделиmodel_name = 'output/tsdae-bert-uncased-math'train_batch_size = 32 max_seq_length = 75num_epochs = 1# Загрузите предварительно обученную модельlocal_model = SentenceTransformer(model_name)# Выберите функцию потерьtrain_loss = losses.MultipleNegativesRankingLoss(local_model)# Используйте 10% данных обучения для разогреваwarmup_steps = math.ceil(len(train_dataloader) * num_epochs * 0.1)# Обучите модельlocal_model.fit(train_objectives=[(train_dataloader, train_loss)],          #evaluator=dev_evaluator,          epochs=num_epochs,          #evaluation_steps=int(len(train_dataloader)*0.1),          warmup_steps=warmup_steps,          output_path=model_save_path,          use_amp=True  # Установите True, если ваш GPU поддерживает операции FP16          )# Сохраните модельfinetuned_model_save_path = 'output/finetuned-bert-uncased-math'local_model.save(finetuned_model_save_path)

Я донастроил модель на всем наборе данных из 500 000 элементов, и это заняло примерно 40 минут на Google Colab Pro при 1 эпохе с размером пакета 32.

Оцените предварительно обученную модель TSDAE и модель с доработкой

Я проведу некоторую предварительную оценку на наборе данных STS (семантическая текстовая сходимость) от HuggingFace, используя EmbeddingSimilarityEvaluator, который возвращает коэффициент ранговой корреляции Спирмена. Однако эти оценки не используют конкретную область, на которую я сфокусирован, и, возможно, не отображают истинную производительность модели. Подробности см. в разделе 4 в [tsdae_article].

Я начинаю с загрузки набора данных от HuggingFace и указания подмножества validation:

import datasets as dtsfrom datasets import load_dataset# Импортируйте набор данных STS-бенчмарк из HuggingFacests = dts.load_dataset('glue', 'stsb', split='validation')

Это объект Dataset в следующем формате:

Dataset({    features: ['sentence1', 'sentence2', 'label', 'idx'],    num_rows: 1379})

Чтобы лучше понять это, давайте рассмотрим одну конкретную запись

# Взглянем на одну из записейsts['idx'][100], sts['sentence1'][100], sts['sentence2'][100], sts['label'][100]>>>(100, 'Женщина едет на лошади.', 'Мужчина переворачивает столы в ярости.', 0.0)

Как мы можем видеть из этого примера, каждая запись имеет 4 признака: индекс, два предложения и метку (которая была создана аннотатором-человеком). Метка может принимать значения от 0 до 5 и измеряет уровень сходства двух предложений (с 5 самым похожим). В этом примере два предложения относятся к совершенно разным темам.

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

Поскольку я буду использовать косинусную схожесть, которая принимает значения от 0 до 1, мне необходимо нормализовать метки:

# Нормализовать диапазон [0, 5] до [0, 1]sts = sts.map(lambda x: {'label': x['label'] / 5.0})

Оберните данные в класс InputExample из библиотеки HuggingFace:

# Создать список для хранения обработанных данныхsamples = []for sample in sts:    # Переформатировать, используя класс InputExample    samples.append(InputExample(        texts=[sample['sentence1'], sample['sentence2']],        label=sample['label']    ))

Создайте оценщик на основе класса EmbeddingSimilarityEvaluator из библиотеки sentence-transformers.

# Создать экземпляр модуля оценкиevaluator = EmbeddingSimilarityEvaluator.from_input_examples(samples)

Вычисляем оценки для модели TSDAE, для модели с настройкой и для нескольких предварительно обученных предложений-трансформеров:

Изображение автора

Таким образом, на общем наборе данных некоторые предварительно обученные модели, такие как all-mpnet-base-v2, превосходят модель TSDAE со значительными улучшениями. Однако, предварительное обучение позволило удвоить производительность исходной модели bert-base-uncased. Можно предположить, что дальнейшая настройка гиперпараметров для настройки модели может привести к лучшим результатам.

Вывод

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

Ссылка на Github на блокнот Colab и пример набора данных.

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

Ссылки

[tsdae_article]. K. Wang и др., Использование трансформатора-основанного последовательного денойзинг-автоэнкодера TSDAE для обучения векторных представлений предложений без учителя (2021) arXiv:2104.06979

[aviation_article]. L. Wang и др., Адаптация предложений-трансформеров для авиационной области (2023) arXiv:2305.09556