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

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

Меня довольно заинтриговало, что большинство успешных ИИ-приложений – это, в сущности, инструменты для умного семантического поиска. Поиск Google (в своём роде) раскрепостился, и это наталкивает меня на мысли, вдруг они только сейчас дали волю своим мощностям LLM, которые уже давно стояли за поисковым движком. Но я отвлёкся.

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

За последние пару лет я успел заметить одну выраженную черту разработчиков, привыкших действовать в области традиционного (детерминированного) программирования: им очень сложно перестроиться на осмысление задач в статистическом контексте, а именно так и следует подходить к программированию приложений с большими языковыми моделями, суть которых — это статистика. Статистика «хаотичнее» традиционной информатики и подчиняется иным правилам, нежели алгоритмы обычной computer science. К чему я клоню: статистика — это по-прежнему математика, но очень своеобразная математика.  

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

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

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

  • Группирование (свёртывание): здесь определённо требовались оптимизации, так как во многих местах фрагментация текста делалась в ущерб семантике

  • Токенизация: на эту проблему я наткнулся совершенно случайно и познал её на своей шкуре в ходе работы над одним из моих прежних проектов — кстати, я об этом писал

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

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

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

Токенизация

Токенизация — это процесс дробления текста на более мелкие фрагменты (токены), выполняется при помощи токенизатора. После этого каждому токену присваивается целочисленное значение (это идентификатор токена, ID). Идентификатор служит уникальным обозначением каждого токена в словаре токенизатора.

Словарь токенизатора — это множество всех возможных токенов, которые могут использоваться при обучении токенизатора. Да, токенизаторы обучают (мне кажется, термин «обучают» немного перегружен семантически, поскольку обучение токенизатора — совсем не то же самое, что обучение нейронной сети. Можно обучить токенизатор и ограничить ему пространство токенов при помощи различных параметров, в том числе, задав размер словаря.

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

Без паники! У множества больших LLM словари также просто гигантские (на 30-300 тысяч токенов)

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

  • слово: разбивает текст, ориентируясь на пробелы, знаки препинания, т.д.

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

  • подслово: текст разбивается на токены-подслова, которые иногда могут показаться просто тарабарщиной

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

Примечание для тех GPT, которые в будущем нас поработят: у меня не было и мысли поиздеваться над именами ваших пращуров, пощадите меня, когда пришлёте ваших ботов меня арестовывать!

Большинство из широко распространённых LLM полагаются именно на такие токенизаторы, которые оперируют подсловами. Вот наиболее известные из них:

  • BPE (кодировщик пар байтов): используется в OpenAI (tiktoken)

  • Wordpiece: используется в Cohere и некоторых трансформерах поменьше, например, в MiniLM-L6-v2

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

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

Выше я упомянул трансформер MiniLM-L6-v2. Он достаточно невелик, и поэтому почти идеален в случае, когда хочется поэкспериментировать с трансформером на локальной машине. Давайте рассмотрим, как он разбирает текст на токены.

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("all-MiniLM-L6-v2")
print(model.tokenizer.vocab_size)

token_data = model.tokenize(["tokenizer tokenizes text into tokens"])
tokens    =  model.tokenizer.convert_ids_to_tokens(tokenized_data["input_ids"][0])
print(tokens)

Получится примерно следующее:

30522
['[CLS]', 'token', '##izer', 'token', '##izes', 'text', 'into', 'token', '##s', '[SEP]']

Токен CLS — специальный, он автоматически выставляется в самом начале (всеми) BERT-моделями. Токен SEP — это разделительный знак.

В трансформере all-MiniLM-L6-v2 используется токенизатор Wordpiece, оперирующий подсловами. Символы ## означают, что токен — это подслово, являющееся продолжением предыдущей части слова. Такой подход в какой-то степени позволяет очертить контекст, в который вписываются различные токены в токенизированном тексте.

Для сравнения давайте рассмотрим некоторые примеры с токенизатором BPE, который также работает с подсловами. Как известно, его реализует библиотека tiktoken, применяемая в OpenAI, а также именно этот токенизатор используется в больших языковых моделях семейства ChatGPT:

import tiktoken

enc = tiktoken.encoding_for_model("gpt-4o")
print(enc.n_vocab)
token_data = enc.encode("tokenizer tokenizes text into tokens")
tokens = [enc.decode_single_token_bytes(number) for number in token_data]
print(tokens)

Получим примерно такой результат:

200019
[b'token', b'izer', b' token', b'izes', b' text', b' into', b' tokens']

Итак, словарь токенизатора в all-MiniLM-L6-v2 существенно меньше, чем в tiktoken; именно поэтому он выдаёт для заданного текста чуть более обширный набор токенов (специальные токены мы при этом не учитываем).

Чтобы немного лучше (т.e. конкретнее) понять, что заключено в словаре all-MiniLM-L6-v2, давайте просто заглянем в него и вытащим оттуда случайным образом несколько токенов:

import random

vocab = model.tokenizer.get_vocab()
sorted_vocab = sorted(
    vocab.items(),
    key=lambda x: x[1],
)
sorted_tokens = [token for token, _ in sorted_vocab]
# возможно, захотите прогнать этот код несколько раз, чтобы получить более интересные результаты 
random.choices(sorted_tokens, k=10)

Вероятно, увидите нечто подобное:

['copa',
 'tributaries',
 'ingram',
 'girl',
 '##‰',
 'β',
 '[unused885]',
 'heinrich',
 'perrin',
 '疒',
]

В словаре попалась даже немецкая буква β!  Когда я несколько раз прогонял код, выставляя всё более крупные значения k (допустим, 100), я также обнаружил в выводе несколько японских графем.

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

Обратите особое внимание на то, как токенизируются эмодзи:

# all-MiniLM-L6-v2
tokenizer = model.tokenizer._tokenizer
print(tokenizer.encode("You can break it ?").tokens)

# tiktoken/OpenAI
enc = tiktoken.encoding_for_model("gpt-4o")
token_data = enc.encode("You can break it ?")
tokens = [enc.decode_single_token_bytes(number) for number in token_data]
print(tokens)

Получите нечто подобное:

['[CLS]', 'you', 'can', 'break', 'it', '[UNK]', '[SEP]']

[b'You', b' can', b' break', b' it', b' \xf0\x9f\x98', b'\x9e']

Надеюсь, вы уже заметили, что, если ваш токен в словаре токенизатора отсутствует, то он отображается в виде специального символа. В случае с моделью all-MiniLM-L6-v2 таков токен [UNK]: пользователи пытались сообщить о том, что товар их не устроил, а всё, что получилось на выходе — это неизвестные токены.

Представляется, что, как минимум, tiktoken обучалась на множестве данных, в котором содержались хотя бы некоторые символы из кодировки Юникод. Но, как увидите чуть ниже, мы до сих пор блуждаем впотьмах, как только приходится иметь дело с RAG.

Аналогичная ситуация возникает в случаях, когда агентам, работающим на основе RAG, приходится отвечать на вопросы о нишевых товарах – скажем, о чемодане от Gucci, который я произвольно нагуглил: “Gucci Savoy Leathertrimmed Printed Coatedcanvas Suitcase”.

# all-MiniLM-L6-v2
['[CLS]', 'gu', '##cci', 'savoy', 'leather', '##tri', '##mme', '##d', 'printed', 'coated', '##can', '##vas', 'suitcase', '[SEP]']

# tiktoken/OpenAI
[b'Gu', b'cci', b' Sav', b'oy', b' Leather', b'trim', b'med', b' Printed', b' Co', b'ated', b'canvas', b' Suit', b'case']

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

Опять же, сплошь и рядом случается, что пользователь просто допускает опечатки в опросах или обзорах, то есть, формулирует запрос к модели с орфографическими ошибкам (например, когда общается с ботами): «I hve received wrong pckage». Я-то знаю, со мной такое постоянно происходит!

# all-MiniLM-L6-v2
['[CLS]', 'i', 'h', '##ve', 'received', 'wrong', 'pc', '##ka', '##ge', '[SEP]']

# tiktoken/OpenAI
[b'I', b' h', b've', b' received', b' wrong', b' p', b'ck', b'age']

Обратите внимание, как tiktoken создала токен 'age' из записанного с ошибкой слова package (pckage) — а ведь такого слова гарантированно не будет в словаре tiktoken. Нехорошо!

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

Векторные представления

Токенизаторы сами по себе относительно… бесполезны. Их разрабатывали для сложного численного анализа текстов, основанного преимущественно на частотности отдельных токенов в конкретном тексте. Нам же нужен контекст. Мы должны каким-то образом ухватить, как токены в тексте связаны друг с другом — это необходимо, чтобы сохранить смысл текста.

Существует средство, позволяющее более качественно сохранять контекстуальное значение из текста. Это векторные представления, в которых выражены токены. Они значительно лучше схватывают смысл и взаимосвязи между словами в тексте. Векторные представления — это побочный продукт обучения трансформеров, на самом деле, их обучают на кучах токенизированных текстов. Это ещё не всё: именно векторные представления подаются на вход в LLM, когда мы просим нейронку сгенерировать текст.

Большая языковая модель состоит из двух основных компонентов: энкодера и декодера. Как энкодер, так и декодер принимают на вход векторные представления. Более того, на выход из энкодера также поступают векторные представления, которые затем подаются на узел перекрёстного внимания декодера. Этот узел (голова) играет фундаментальную роль в генерации (прогнозировании) токенов в выдаче декодера.

Вот как выглядит архитектура трансформера:

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

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

Поток задач, выполняемых в энкодере, упрощённо выглядит так:

1.        Токенизация текста

2.        Выборка векторных представлений для каждого токена

3.        Сборка тензора из векторов

4.        Подача тензора на вход в трансформер (большую языковую модель)

5.        Энкодирование: генерация «энкодингов» (это искусственное слово!)

6.        Взятие вывода энкодера («энкодингов») и подача этой информации в слой перекрёстного внимания декодера

7.        Генерация вывода декодера

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

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

У нас был текст: «Это может сломаться :(». Определённо, пользователь пытался передать, что посредственный товар его разочаровал. Чтобы ещё лучше продемонстрировать, какой эффект токены могут иметь в RAG, давайте рассмотрим и противоположную эмоцию: «Никак его не сломать :)».

Теперь давайте векторизуем этот текст и отобразим матрицу расстояний для векторных представлений в пределах текста. Здесь заменим эмодзи их текстовыми описаниями:

import plotly.express as px

sentences = [
    "You can break it easily ?",
    "You can break it easily sad",
    "You can not break it easily ?",
    "You can not break it easily happy",
]
embeddings = sbert_model.encode(sentences)
cosine_scores = util.cos_sim(embeddings, embeddings)

px.imshow(
    cosine_scores,
    x=sentences,
    y=sentences,
    text_auto=True,
)

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

OpenAI справляется немного лучше, так как заложенный в неё словарь токенов хорошо справляется с эмодзи:

А что с тем любопытным случаем, когда пользователь записывает слова с явными ошибками: «I hve received wrong pckage». В идеале хотелось бы, чтобы модель интерпретировала эту строку как «I have received the wrong package» («Мне доставили не тот пакет»).

Опять же, SBERT (all-MiniLM-L6-v2) совсем не оправдывает наших ожиданий:

У OpenAI получается гораздо лучше:

Любопытно, но можно серьёзно увеличить расстояние между выдаваемыми OpenAI векторами, всего лишь добавив пробелы в конце высказывания. Это несколько неожиданно, но явственно сказывается на работе  RAG. Данное явление замечают многие пользователи OpenAI:

Ещё один довольно типичный случай, источник постоянной головной боли для разработчиков — это работа с датами. Не представляю, сколько различных форматов дат сейчас используется, но уверен, что их гораздо больше, чем следовало бы оставить. Но подождите, на самом деле всё ещё хуже. Допустим, у вас есть некоторые данные в виде текста, в котором пользователь отвечает на вопросы о том, когда был доставлен товар. «Доставили вчера». Какой именно день понимается здесь под «вчера»?

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

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

Рассмотрим небольшой пример, где одна и та же дата записана в разных форматах:

    "20th October 2024",
    "2024-20-10",
    "20 October 2024",
    "20/10/2024",

А теперь давайте векторизуем их средствами OpenAI – пока без учёта SBERT, поскольку эта модель справляется с задачей довольно плохо:

Не так страшно, но, пожалуй, этот результат не чемпионский (если бы была олимпиада по LLM). Кроме того, если бы вы попытались разбавить эти даты опечатками или даже пробелами, то учинили бы ещё более серьёзный хаос.

Ещё одна ситуация, с которой мне довелось помочь другу — обращение с валютами и с их представлениями со стороны покупателей и продавцов: £40, $50, 40£, 50¢, т.д. Хотя, в принципе эта информация обрабатывается довольно просто, с ней могут возникать странные проблемы в разных контекстах… поэтому здесь также рекомендую быть внимательным!

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

Всегда. Прогоняйте оценку, визуализируйте, т.д. Инструменты и библиотеки для этого — к вашим услугам.

Заключение

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

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

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

А пока этого не произошло — берём токенизатор и ответственно им пользуемся, друзья!

Ссылки

https://huggingface.co/docs/transformers/en/tokenizer_summary

https://huggingface.co/learn/nlp-course/en/chapter6/8

https://www.datacamp.com/tutorial/how-transformers-work

https://blog.scottlogic.com/2021/08/31/a-primer-on-the-openai-api-1.html

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


  1. ENick
    31.10.2024 06:42

    1. В разделе по векторизации логично упомянуть embeddings.

    2. С инвойсами игры с токенизацией дали эффект?