Розмір частин та перекриття в RAG: крок, обмеження на токени та з’єднання
Як розділити довгу книгу на частини для RAG: вибір розміру та ступеня перекриття, розуміння параметра stride, дотримання ліміту токенів для ембеддингу, зберігання метаданих та перевірка ступеня перекриття під час реального розділення.
Візьміть автобіографію Бенджаміна Франкліна з Project Gutenberg. Вставляння всієї книги в запит є марнотратством, навіть якщо вікно контексту може її вмістити. На запитання про те, чому Франклін хотів, щоб Британія зберегла Канаду, відповідь, ймовірно, міститься у двох-трьох уривках, а решта сторінок займе контекст та приховає докази. Для таких ситуацій існує технологія Retrieval-augmented generation (RAG). Система знаходить уривки, які стосуються запитання, та передає лише їх мовній моделі. Книга залишається у сховищі, а система вибирає необхідні докази.
Ембеддинги та причини розділення тексту
RAG починається з перетворення фрагментів тексту на ембеддинги — числові представлення значення. Фрагменти з схожим змістом розташовуються близько один до одного у високорозмірному векторному просторі, навіть якщо у них різна формулювання. Коли надходить запит, він також перетворюється на ембеддинг, знаходяться найближчі збережені вектори, і повертаються відповідні фрагменти тексту. Вектор визначає місцезнаходження тексту, а саме цей текст і читає модель.
Один ембеддинг не може представити цілу книгу. Один вектор, що описує життя Франкліна, змішає його дитинство в Бостоні, експерименти з друку та електрикою, твір «Бідний Річард» та свідчення в Парламенті в щось, що погано описує жоден з цих аспектів. Тому текст потрібно розділити на частини перед його ембеддингуванням.
Розмір, перекриття та крок
Розумна відправна точка:
DEFAULT_CHUNK_SIZE = 800 # characters
DEFAULT_CHUNK_OVERLAP = 150 # characters
Люди часто сприймають їх як два незалежні регулятори. Насправді вони визначають одну річ — наскільки вікно рухається вперед з кожним кроком:
stride = chunk_size − chunk_overlap
= 800 − 150
= 650 characters
Цей крок — це кількість нового тексту, який додається кожним новим фрагментом.
Розглядайте розмір фрагменту як максимальний ліміт, а не як ціль. Засоби розбиття воліють робити розріз у природному місці — чи то після абзацу, рядка, речення чи слова, тому запит на 800 символів може призвести до фрагментів розміром 640, 731 або саме 800.
Перекриття — це текст, який є спільним для сусідніх фрагментів, і він справді виконує корисну функцію. Без нього розріз може залишити такий відокремлений фрагмент:
"…but left school at the age of ten."
Сама по собі ця рядок мало що говорить. З наявністю перекриття наступний фрагмент зберігає достатньо попереднього контексту, щоб залишатися читабельним та знаходжимою інформацією. Це особливо важливо для довгих речень та тісно пов’язаних аргументів. Лист Франкліна про Канаду містить довгу послідовність речень, які можуть перетинатися межею, через що один фрагмент залишається без висновку, а наступний — без посилання. Невелике перекриття зберігає зв’язок у обох записах.
Ціною є дублювання. Сусідні записи зі схожим формулюванням можуть бути знайдені разом, що є нормальним. Коли дубліки витісняють кращі докази, метод, який враховує різноманітність, такий як максимальна маржинальна релевантність (MMR), допомагає.
Що містить кожен збережений фрагмент
Кожен фрагмент містить чотири поля:
id
document
embedding
metadata
Приклад запису:
id 7c2a1e90-4b11-4d3e-9f08-12a6c0e84b21
document "His boyhood in Boston was a stern beginning of the habit
of hard work and rigid economy which marked the man. For
a year he went to the Latin Grammar School on School
Street, but left off at the age of ten to help his father
in making soap and candles."
embedding 384 floats — [-0.0412, 0.0187, 0.0621, -0.0094, 0.0330, …]
norm = 1.0
metadata {
book: "Franklin's Autobiography",
source: "https://www.gutenberg.org/cache/epub/36151/pg36151-images.html",
gutenberg_id: 36151,
page_label: "5",
chunk_index: 2
}
Ембеддинги використовуються для пошуку, а оригінальний текст — для читання, цитування та посилань. Метадані також потребують уваги. Якщо видання містить позначки сторінок у форматі [Pg 5], збережіть їх, щоб посилання вказувало на сторінку, яку читачі можуть знайти у своєму примірнику.
Перед індексацією видаліть матеріали низької цінності: титульні сторінки, шаблонний текст, умови ліцензії, повторювані заголовки та списки приміток. Вони створюють слабкі ембеддинги та погіршують якість пошуку.
Нормалізовані вектори
Вектори ембеддингів зазвичай нормалізуються за формулою L2, що зменшує кожен з них до довжини 1:
‖v‖ = √(v₁² + v₂² + … + vₙ²)
Схожість косинуса порівнює два вектори наступним чином:
cos θ = (a · b) / (‖a‖ ‖b‖)
Коли обидва мають одиницю довжини, це зводиться до звичайного скалярного добутку:
If ‖a‖ = ‖b‖ = 1:
cos θ = a · b
Тож рейтинг відображає, наскільки тісно два значення спрямовані в одному напрямку, а модуль вектора не враховується, що дозволяє здійснювати швидке та змістовне пошук.
Токени, а не символи
Налаштування у одиницях символів можуть приховувати жорстку межу, оскільки моделі ембеддингу рахують токени. Модель all-MiniLM-L6-v2 приймає максимум 256 токенів. Довший фрагмент може бути обрізаний без жодних попереджень. База даних все ще містить повний текст, але її ембеддинг охоплює лише початкову частину, а обрізана кінцева частина стає важко або неможливо дошукати. Обережна оцінка:
maximum safe chunk size in characters
≈ model token limit × 4
Для моделі з 256 токенами це дає приблизно 1 000 символів. Використання чотирьох символів на токен залишає запас, адже діалоги, скорочення, пунктуація та форматований текст можуть бути розділені на токени щільніше, ніж проза.
Новіші моделі приймають значно довші вхідні дані, але розділення їх на частини все одно має значення. Більший обсяг даних не вирішує проблеми отримання інформації, оскільки надто широкі частини змішують несуміжні теми в один розмитий вектор.
Процес обробки
Спочатку необхідно розділити завантажені сторінки на частини:
from rag_qa.gutenberg import load_pages
from rag_qa.chunk import chunk_documents
from rag_qa.config import load_settings
s = load_settings()
print(s.summary())
# {
# 'chunk_size_chars': 800,
# 'chunk_overlap_chars': 150,
# 'stride_chars': 650, # size − overlap; this sets chunk count
# 'model': 'all-MiniLM-L6-v2',
# 'model_max_tokens': 256, # hard cap; overflow is silent
# 'embedding_dims': 384,
# 'max_safe_chunk_chars': 1024, # 256 × 4
# 'book': "Franklin's Autobiography",
# 'gutenberg_id': 36151,
# }
pages = load_pages()
chunks = chunk_documents(pages, s.chunk_size, s.chunk_overlap)
print(len(pages), len(chunks), s.stride)
По-друге, перевірте кожен фрагмент щодо ліміту токенів моделі:
from sentence_transformers import SentenceTransformer
from rag_qa.tokens import check_chunk
m = SentenceTransformer("all-MiniLM-L6-v2")
print(m.max_seq_length) # 256
for c in chunks:
r = check_chunk(c)
if r.truncated:
print("silent truncate:", r.n_chars, "chars /", r.n_tokens, "tokens")
Скрипт виводить максимальну довжину моделі, а потім перераховує лише ті фрагменти, які будуть безшумно вирізані. При звичайному запуску виводиться лише цей ліміт:
256
Коли все поміщається, ви бачите лише це. Якщо деякі частини перевищують ці межі, з’являються додаткові рядки:
256
silent truncate: 1820 chars / 412 tokens
silent truncate: 960 chars / 301 tokens
Як читати ці рядки:
По-третє, подивіться, що у кінцевому підсумку зберігається:
from rag_qa.store import ingest, open_store
store = open_store()
ingest(chunks, store)
row = store.get(limit=1)
print(row["ids"][0])
print(row["documents"][0][:200])
print(len(row["embeddings"][0]), row["metadatas"][0])
# 384 floats, norm 1.0, metadata.page_label == printed [Pg N]
Цей фрагмент демонструє один шматок після обробки та виводить три рядки:
doc-12-p3-c0
She left school at the age of ten. The next sentence continues on the same page…
384 {'page_label': '3', 'source': 'notes.pdf'}
Четверте, зробіть запит:
from rag_qa.retrieve import search, search_mmr
q = "why did Franklin want Britain to keep Canada"
for hit in search(q, k=5):
print(f"{hit['score']:.3f} p.{hit['metadata']['page_label']} {hit['document'][:120]}")
# overlap makes near-duplicate hits; MMR trades a little score for diversity
for hit in search_mmr(q, k=5):
print(hit["metadata"]["page_label"], hit["score"])
Приклад результатів:
0.812 p.7 I have long been of opinion that the foundations of the future grandeur and stability of the British empire lie in America
0.781 p.7 they are, nevertheless, broad and strong enough to support the greatest political structure that human wisdom ever yet
0.744 p.7 I am, therefore, by no means for restoring Canada. If we keep it all the country from the St. Lawrence to the Mississippi
0.691 p.8 I left England about the end of August, 1762, in company with ten sail of merchant ships
0.640 p.6 In this Autobiography Franklin tells of his own life to the year 1757, when he went to England
Кілька найпопулярніших результатів часто походять з однієї й тієї самої сторінки. Це прояв перекриття, адже сусідні фрагменти одного речення повністю збігаються.
Перекриття на невеликій вибірці
Візьміть короткий уривок довжиною близько 190 символів з вступу книги, у якому описується дитинство автора в Бостоні: ранні звички наполегливої праці та ощадливості, рік навчання в Латинській граматичній школі на вулиці Скул-стріт та вихід з неї у віці десяти років. Розділіть його на фрагменти розміром по 60 символів кожен.
Якщо немає перекриття, інструмент розділення робить розріз саме на межі:
[0] 59 His boyhood in Boston was a stern beginning of the habit of
[1] 55 hard work and rigid economy which marked the man. For a
[2] 58 year he went to the Latin Grammar School on School Street,
[3] 31 but left off at the age of ten.
Частинка 0 закінчується на „звички“, а частинка 1 починається з „важкої праці“, причому між ними немає жодного зв’язку. Фраза „звичка до важкої праці“ ніколи не зустрічається повністю в жодній з частинок. Якщо розріз відбувається посеред речення, кожен вектор кодує лише половину ідеї, тож запит, який потребує повного речення, погано збігається з обома частинами.
З перекриттям у 15 символів:
[0] 59 His boyhood in Boston was a stern beginning of the habit of
[1] 57 ⟦the habit of⟧ hard work and rigid economy which marked the
[2] 55 ⟦marked the⟧ man. For a year he went to the Latin Grammar
[3] 58 ⟦Latin Grammar⟧ School on School Street, but left off at the
[4] 22 ⟦off at the⟧ age of ten.
Маркер нижче показує, який текст повторюється з попереднього фрагмента:
⟦…⟧ = text repeated from the previous chunk
Жоден фрагмент не досягає 60 символів, що знову демонструє, що цей розмір є лише верхньою межею. Пропуск перекриттів дає менший індекс та усуває дублікати, але все, що випадково потрапляє під розрізання, втрачається. Перекриття забезпечує збереження цих з’єднань та не є додатковим контекстом заради самого себе.
Чотири завдання та інструменти для їх виконання
RAG складається з чотирьох завдань: завантаження тексту, його розрізання, вбудовування та зберігання та пошук векторів. Жодна окрема бібліотека не є „RAG“. Інструменти для розрізання та координації:
Моделі вбудовування:
Популярні бази даних векторів доповнюють цей набір.