Размах частак і перакрыцце ў 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 сімвалаў з адмовы кнігі, якая опісвае дзецінство аўтара ў Бостане: раннія прычыны аддачлівасці і эканаміі, годзіну навчання ў Латынскай граматычной школе на вуліцы School Street, а таксама пакіданне школы у возрасце дзесяці гадоў. Раздзеліце яго на фрагменты па 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“. Інструменты для адсечэння і коордынацыі:
Мадэлі для вбудоўвання:
Популярныя базы дадзеных вектараў дапамагают завершыць весь комплект.