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

Меня зовут Вадим Скляров, и я бизнес-аналитик в MWS. Казалось бы, тема векторизации текстов сугубо техническая, зачем мне в нее погружаться? Ответ прост: чтобы понимать возможности и ограничения работы с векторными представлениями и задавать правильные вопросы заказчику, не обещать того, чего сделать не получится. Плюс это помогает точнее оценить сроки и стоимость проекта.

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


Главная идея векторизации текстов

Embedding-модель преобразует текст произвольного объема в числовой вектор фиксированной длины N. В математическом понимании его можно представить как точку в N-мерном пространстве, а в человеческом — как смысл текста. Отсюда главная идея применения:
«Если два вектора, то есть две точки, в N-мерном пространстве расположены близко, значит, тексты, соответствующие им, похожи по смыслу».

Аналитику важно разделять:

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

  • Сходство по содержанию. В похожих текстах много одинаковых слов (токенов), но по своему смыслу материалы могут быть различны. Хороший пример этого подхода — поиск по базе нормативно-справочных документов: словарь терминов ограничен и известен, смысл текстов однозначен.

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

Что важно учитывать при расчете проектов

Выбор модели векторизации

Доступные модели преобразования текста в вектор (embedding) под любые разновидности данной задачи можно найти например на HuggingFace.

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

 Правильно выбрать модель помогут эти вопросы:

  1. Зачем нужна векторизация текстов? Задач может быть много: классификация, суммаризация, выделение тональности, поиск релевантных фрагментов и так далее. Для каждого типа будет свой размер текста, смысл которого нужно вместить в один вектор. Например, для определения тональности исходные фрагменты короткие, для суммаризации — длинные.

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

  2. На каких языках написаны тексты, которые будут векторизоваться? Embedding-модели обучается преобразованию на мультиязычных датасетах. Если для русскоязычного текста использовать модель, незнакомую с ним, она сгенерирует вектор, в котором закодированного смысла будет мало.

  3. Какая скорость векторизации должна поддерживаться на проекте? Преобразование текста в вектор (включая такие сопутствующие задачи, как парсинг, разделение на фрагменты, очистка и подготовка) — ресурсоемкая операция. К тому же в некоторых проектах новые тексты добавляются постоянно, а значит, и скорость обновления векторной базы может быть высокой. Поэтому при выборе LLM и ее окружения (CPU или GPU) эта характеристика — одна из ключевых.

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

Оценка расстояния между векторами

Именно через нее оценивается схожесть текстов по смыслу. Способов (метрик) существует несколько, нужный выбирается после ответа на вопрос: «Для решения какой задачи используется векторная база?».

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

Чтобы понимать результаты оценки сходства векторов, надо знать, какая метрика расстояния была выбрана.

Важность максимальной длины контекста

Этот параметр показывает количество токенов, которое модель может обработать за один раз. Если длина текста меньше или равна ему, LLM при генерации закодирует в вектор весь смысл. Если же нет — обрежет текст по длине контекста: часть смысла не попадет в сгенерированный вектор и он не будет соответствовать тексту.


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

  1. Возьмем в качестве embedding-модели векторизации cointegrated/LaBSE-en-ru, максимальная длина ее контекста — 512 токенов.

  2. Создадим два текста в любой генеративной LLM-модели, например DeepSeek, с помощью следующего промпта:

сгенерируй два текста на русском языке. требования:
- каждый текст должен быть длиной 512 токенов,
- косинусное сходство между векторами этих текстов должно быть максимально близким к 0,
- векторизация текстов должна быть выполнена моделью LaBSE-en-ru
  1. Рассчитаем косинусное сходство между парами текстов:

Текст 1 <-> Текс 2 : 0.4916
Документ (Текст 1 + Текст 2) <-> Текст 1: 0.9999
Документ (Текст 1 + Текст 2) <-> Текст 2: 0.4916

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

Когда объем текста превышает этот параметр, необходимо либо выбрать модель с большим контекстом, либо использовать различные технические приемы, чтобы снизить риски генерации «неточных» векторов. Например, разбивать текст на семантически целостные фрагменты.

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

Фрагментация текста

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

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

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

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

  • Текст имеет четкую структуру (например, база договоров или нормативных документов). Каждый заголовок соответствует одному смыслу — текст удобно делить по ним, если это контекст модели.

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

Неправильный подход к фрагментации снизит эффективность работы с векторной базой. Как и в предыдущем разделе, это приведет к потере времени на повторную векторизацию.

Векторизация структурированного текста

Структурированным считается текст, который содержит такие артефакты, как заголовки, таблицы, изображения, блоки кода и так далее. Каждый из них обозначается специальными символами. Например, заголовок в html-документе выделен через теги <h1>Заголовок первого уровня</h1>, а в markdown — последовательностью спецсимволов ## Заголовок второго уровня.

Зачем это нужно знать аналитику?

  1. Структура усиливает контекст и влияет на границы фрагментов. Заголовок «Права и обязанности исполнителя» в договоре явно указывает на смысл абзацев под ним, а блок кода, ограниченный спецсимволами (например, с тегом <pre>), должен попасть в один фрагмент при разделении текста на части. Таким образом, учет структуры повышает качество генерируемых векторов.

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

  3. Есть разные подходы к векторизации с учетом структуры. Можно использовать специально обученные модели либо выполнять предварительную обработку текста: заменять символы разметки особыми токенами (<h1> -> [H1]) или их явным описанием (<h1> -> "ЗАГОЛОВОК УРОВНЯ 1:")

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

Переходим к практике: как применить полученные знания о векторизации

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

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

Задача от бизнеса: «Реализовать поиск релевантных по смыслу документов в базе текстов»

Если вы внимательно читали этот материал, то ваш ход рассуждений должен быть таким:

А. Векторизация текстов с помощью embedding-моделей решает задачу кодирования их смыслов. Это соответствует запросу бизнеса.

B. Чтобы правильно выбрать модель, прежде всего нужно из существующей базы выделить и проанализировать один или несколько репрезентативных текстов.

Пусть для нашего примера он будет таким.

<h1>Функция console.log() в JavaScript</h1><p>Метод console.log() — это встроенная функция JavaScript, предназначенная для вывода информации в консоль браузера или серверной среды (например, Node.js). Это один из самых популярных инструментов отладки, позволяющий разработчикам проверять значения переменных, логировать выполнение кода и анализировать ошибки.</p><h2>Основные возможности console.log()</h2><p>Функция console.log() принимает один или несколько аргументов и выводит их в консоль в удобочитаемом формате. Она поддерживает различные типы данных: строки, числа, объекты, массивы и даже HTML-элементы (в браузерных средах).</p><pre>console.log('Hello, World!'); console.log(42);</pre><h2>Дополнительные особенности</h2><p>Помимо простого вывода, console.log() поддерживает форматирование с помощью спецификаторов. Также можно передавать несколько аргументов для объединения вывода.</p>"

С. Текст структурирован html-разметкой. Одна из моделей, которая понимает ее, — MarkupLM. Но нам она не подходит: обучена на англоязычном корпусе, поэтому русскоязычный текст векторизует, скорее всего, с низким качеством. Для нашего языка на роль фокус-моделей возьмем LaBSE-en-ru и rubert-tiny2.

D. Не забываем о максимальном размере контекста. Если исходный текст превышает его, необходимо применить фрагментацию. У LaBSE-en-ru контекст — 512 токенов, у rubert-tiny2 — 2048. Наш репрезентативный текст в них попадает, поэтому фрагментация не потребуется.

E. Для количественной оценки релевантности документов поисковому запросу используем косинусное сходство. Сам запрос для проверки гипотезы пусть будет таким:
Приведи пример использования console.log(). Опиши его основные возможности.

Он близок по смыслу репрезентативному тексту и в то же время сформулирован так, чтобы не являться его частью.

F. Модели фокус-группы не понимают структуры текста, заданной html-разметкой. Для предварительной обработки попробуем использовать два подхода:

  1. Очистим текст от тегов.

  2. Заменим теги на спецтокены

G. И наконец, сформулируем гипотезу для проверки:

  • Для задачи бизнеса эффективное решение — формирование векторизованной базы текстов с применением одной из моделей фокус-группы: LaBSE-en-ru или rubert-tiny2.

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

  • Для оценки релевантности применяем косинусное сходство между векторами.

В качестве языка программирования, позволяющего быстро проверить гипотезу, как правило, используют Python, а для взаимодействия с LLM можно взять Langchain.

Для нашего примера код будет выглядеть ориентировочно так:
from langchain_text_splitters.html import HTMLSemanticPreservingSplitter
from bs4 import Tag
from transformers import AutoTokenizer, AutoModel
import torch
from functools import partial
import numpy as np

# Функция расчета косинусного сходства
def cosine_similarity(query_vector, document_vector):
    dot = np.dot(query_vector, document_vector)
    norm_query = np.linalg.norm(query_vector)
    norm_document = np.linalg.norm(document_vector)
    return dot / (norm_query * norm_document)

# Функция обработки тегов
def tag_extractor(tag: Tag, mapped_tags):
    text = tag.get_text(separator="", strip=True)
    if tag.name in mapped_tags:
        return f'{mapped_tags[tag.name]} {text}'
    return f"{text}"

# Функция предоразования текста в вектор
def get_embedding(text, model, tokenizer):
    t = tokenizer(text, return_tensors='pt', padding=True, truncation=True)
    with torch.no_grad():
        model_output = model(**t)
    embeddings = model_output.last_hidden_state[:, 0, :]
    return embeddings[0].cpu().numpy()

if __name__ == '__main__':
    text = "исходный_текст"
    query = "эталонный_текст"
	# Маппинг спецтокенов для замены тегов
    mapped_tags = {"h1": "[H1]", "h2": "[H2]", "pre": "[CODE]", "p": "[TEXT]"}

    # создаем два предобработчика текста
    # 1. удаляет из текста все теги
    text_preprocessor = HTMLSemanticPreservingSplitter(headers_to_split_on=[])
    # 2. заменяет теги спецтокенами
    handler = partial(tag_extractor, mapped_tags=mapped_tags)
    text_preprocessor_with_prepared_tags = HTMLSemanticPreservingSplitter(
        headers_to_split_on=[],
        custom_handlers={_: handler for _ in mapped_tags.keys()}
        )

    for model_name in ["cointegrated/LaBSE-en-ru", "cointegrated/rubert-tiny2"]:
        tokenizer = AutoTokenizer.from_pretrained(model_name)
        model = AutoModel.from_pretrained(model_name)
		# Добавляем спецтокены в модель
        tokenizer.add_tokens(list(mapped_tags.values()))
        model.resize_token_embeddings(len(tokenizer))

        print (f"Модель: {model_name}")
        emb_cleaned_text = get_embedding(text_preprocessor.split_text(text)[0].page_content, model, tokenizer)      
        emb_unprepared_tags  = get_embedding(text, model, tokenizer)
        emb_prepared_tags  = get_embedding(text_preprocessor_with_prepared_tags.split_text(text)[0].page_content, model, tokenizer)
        emb_query  = get_embedding(query, model, tokenizer)

        print(f'\tОчищенный текст <-> Не обработанный текст: {cosine_similarity(emb_cleaned_text, emb_unprepared_tags)}')
        print(f'\tОчищенный текст <-> Текст со спецтокенами: {cosine_similarity(emb_cleaned_text, emb_prepared_tags)}')
        print (f'\t{"-"*40}')
        print(f'\tЗапрос <-> Не обработанный текст: {cosine_similarity(emb_query, emb_unprepared_tags)}')
        print(f'\tЗапрос <-> Очищенный текст: {cosine_similarity(emb_query, emb_cleaned_text)}')
        print(f'\tЗаброс <-> Текст со спецтокенами: {cosine_similarity(emb_query, emb_prepared_tags)}')

Результат

Модель: cointegrated/LaBSE-en-ru
        Очищенный текст <-> Не обработанный текст: 0.9534
        Очищенный текст <-> Текст со спецтокенами: 0.9897
        ----------------------------------------
        Запрос <-> Не обработанный текст: 0.61044
        Запрос <-> Очищенный текст: 0.6817
        Запрос <-> Текст со спецтокенами: 0.6597
Модель: cointegrated/rubert-tiny2
        Очищенный текст <-> Не обработанный текст: 0.9657
        Очищенный текст <-> Текст со спецтокенами: 0.9918
        ----------------------------------------
        Запрос <-> Не обработанный текст: 0.6891
        Запрос <-> Очищенный текст: 0.6918
        Запрос <-> Текст со спецтокенами: 0.6910

Выводы:

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

  2. В приведенном примере очистка от html-тегов дает лучший результат, чем их замена спецтокенами. При этом модель LaBSE-en-ru снижает релевантность документов при наличии в них «шумовых» артефактов, что в данном случае является преимуществом.

Таким образом, при описании задачи в разработку следует указать, что для векторизации стоит использовать модель LaBSE-en-ru, а для предварительной обработки — очистку от html-тегов.

Вместо заключения

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

В этой публикации не освещены еще две обширные темы:

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

  2. Хранение векторов. Для этого используются векторные хранилища и базы данных — как векторные, так и реляционные. Выбор способа существенно влияет на производительность и масштабируемость проекта.

Как с ними работает аналитик, расскажу в следующих материалах.

Комментарии (0)