Нарязване на документи (chunking)
Накратко: Нарязването (chunking) дели документа на парчета по думи или изречения, със застъпване, така че всяко парче да има свой вектор, а отговорът да не се среже.
Проблем
Цялата страница в един вектор
Магазинът има политика за връщане. Тя е една страница с шест изречения:
# политиката за връщане: шест изреченияPOLICY = [ "можете да върнете стоката до 14 дни от доставката", "стоката трябва да е в оригиналната опаковка", "куриер ще я вземе от адреса ви безплатно", "парите се връщат по картата до пет дни", "стоки с ваш надпис не се връщат", "за повреда пишете ни до два дни",]
# въпросът на клиентаQUESTION = "кога ще ми върнат парите по картата"Клиентът пита „кога ще ми върнат парите по картата“. Отговорът е четвъртото изречение. Ако цялата страница е един текст в индекса, тя е един вектор:
print(f"думи: {len(words)}")doc = " ".join(POLICY)print(f"целият документ: {cosine(v, space.of_text(doc)):.3f}")print(f"изречението с отговора: {cosine(v, space.of_text(POLICY[3])):.3f}")думи: 46целият документ: 0.710изречението с отговора: 0.874Цялата страница е 0,710, а изречението с отговора само е 0,874. Векторът на страницата е смес от шест теми: срок, опаковка, куриер, пари, изключения, повреди. Колкото по-дълга е страницата, толкова повече се размива. В база с много документи такава страница губи от кратки текстове, които изобщо не отговарят на въпроса.
А ако все пак бъде намерена, в промпта отиват всичките 46 думи заради 8. Истинските политики са хиляди думи, и три такива страници пълнят прозореца на контекста и сметката.
Идеята
Парчета
Документът се реже на парчета (chunks), преди да влезе в индекса. Всяко парче е отделен запис: своят текст, своят вектор и кой документ е. Търсенето от предишния урок намира парчето, а не цялата страница.
Най-простото рязане е по брой думи: първите 10 думи, следващите 10 и така до края. За векторите в урока ползваме същия модел като в урока за векторите, с две разлики: той се учи и от текста на политиката (иначе думите ѝ са непознати), и има 16 оси вместо 8. Всички парчета са за връщане и с 8 оси се различават слабо.
space = Space(TEXT + POLICY, STOP, 16)words = " ".join(POLICY).split(" ")v = space.of_text(QUESTION)Виж го
Парчета по 10 думи
Функцията реже думите на парчета. Засега застъпването е 0: всяко парче започва там, където свършва предишното.
# парчета по size думи; стъпката е size - overlapdef chunk(words, size, overlap): pieces = [] step = size - overlap for start in range(0, len(words), step): end = min(start + size, len(words)) pieces.append(" ".join(words[start:end])) if end == len(words): break return piecesДруга функция печата всяко парче с близостта му до въпроса и кое е най-близко:
# всяко парче с близостта му; най-близкото накраяdef show(space, v, pieces): best, top = 0, -1.0 for i, p in enumerate(pieces): c = cosine(v, space.of_text(p)) print(f"{i + 1} {c:.3f} {p}") if c > top: best, top = i, c print("→", best + 1)show(space, v, chunk(words, 10, 0))1 0.652 можете да върнете стоката до 14 дни от доставката стоката2 0.174 трябва да е в оригиналната опаковка куриер ще я вземе3 0.815 от адреса ви безплатно парите се връщат по картата до4 0.745 пет дни стоки с ваш надпис не се връщат за5 0.632 повреда пишете ни до два дни→ 3Най-близко е парче 3 (0,815). То свършва на „по картата до“, а „пет дни“ е в парче 4. Моделът ще прочете „парите се връщат по картата до“ и няма да знае колко. Срезът е паднал точно в средата на отговора.
Идеята
Застъпване
Срезът е сляп: не знае къде свършва изречението. Поправката е застъпване (overlap): всяко парче започва малко преди края на предишното. С парчета по 10 думи и застъпване 4 всяко парче започва 6 думи след предишното, а последните му 4 думи са първите 4 на следващото.
Така краищата се повтарят и изречение, срязано в едно парче, е цяло в съседното. Цената е повече парчета и повторени думи.
Виж го
Със застъпване 4
show(space, v, chunk(words, 10, 4))1 0.652 можете да върнете стоката до 14 дни от доставката стоката2 0.436 дни от доставката стоката трябва да е в оригиналната опаковка3 0.147 е в оригиналната опаковка куриер ще я вземе от адреса4 0.531 я вземе от адреса ви безплатно парите се връщат по5 0.874 парите се връщат по картата до пет дни стоки с6 0.745 пет дни стоки с ваш надпис не се връщат за7 0.732 не се връщат за повреда пишете ни до два дни→ 5Сега най-близко е парче 5 (0,874) и то държи цялото изречение: „парите се връщат по картата до пет дни“. Парчетата са 7 вместо 5.
# парчета по size думи; стъпката е size - overlapdef chunk(words, size, overlap): pieces = [] step = size - overlap for start in range(0, len(words), step): end = min(start + size, len(words)) pieces.append(" ".join(words[start:end])) if end == len(words): break return pieces
v = space.of_text(QUESTION)show(space, v, chunk(words, 10, 0))# парчета по size думи; стъпката е size - overlapdef chunk(words, size, overlap): pieces = [] step = size - overlap for start in range(0, len(words), step): end = min(start + size, len(words)) pieces.append(" ".join(words[start:end])) if end == len(words): break return pieces
v = space.of_text(QUESTION)show(space, v, chunk(words, 10, 0))# парчета по size думи; стъпката е size - overlapdef chunk(words, size, overlap): pieces = [] step = size - overlap for start in range(0, len(words), step): end = min(start + size, len(words)) pieces.append(" ".join(words[start:end])) if end == len(words): break return pieces
v = space.of_text(QUESTION)show(space, v, chunk(words, 10, 4))Стъпка по стъпка, с думи
- Политиката за връщане: 46 думи. В оранжево е изречението с отговора: „парите се връщат по картата до пет дни“.
- Парчета по 10 думи без застъпване: 5 парчета, едно след друго.
- Близостта на всяко парче до въпроса. Най-близко е парче 3 (0,815).
- Но парче 3 свършва на „по картата до“: „пет дни“ е в следващото парче. Отговорът е срязан.
- Застъпване 4: всяко парче започва 6 думи след предишното и краищата се повтарят. 7 парчета.
- Най-близко е парче 5 (0,874) и то държи цялото изречение.
- Без застъпване отговорът е срязан между две парчета, а със застъпване едно парче го държи цял. Цената: 7 парчета вместо 5 и повторени думи.
Пробвай
Предвиди изхода
Изречение от 8 думи на парчета по 4 със застъпване 1. Колко парчета излизат?
ws = "доставката до офис на куриер струва 3 евро".split(" ")print(len(chunk(ws, 4, 1)))Стъпката е 4 − 1 = 3. Парчетата започват на думи 0, 3 и 6. Третото е само две думи, „3 евро“, и стига до края, затова цикълът спира. Без застъпване щяха да са две парчета.
Пробвай
Попълни
С колко думи по-късно започва всяко следващо парче? Какво стои на мястото на ___?
# парчета по size думи; стъпката е size - overlap
def chunk(words, size, overlap):
pieces = []
step = size -
for start in range(0, len(words), step):
end = min(start + size, len(words))
pieces.append(" ".join(words[start:end]))
if end == len(words):
break
return piecesoverlap. Стъпката е размерът минус застъпването: с 10 и 4 следващото парче започва 6 думи по-късно, а 4 думи се повтарят.
Пробвай
Намери грешката
Колега реже със застъпване 4, но част от политиката не е в нито едно парче. На кой ред е грешката?
Избери реда с грешката.
Стъпката е size + overlap: парчетата започват на 0, 14, 28 и 42 и между тях остават по 4 думи, които не влизат никъде. Тези думи никога няма да бъдат намерени. Трябва size - overlap.
Идеята
По изречения
Брой думи е грубо. Документите имат естествени граници: изречения, абзаци, заглавия. Ако парчето е цяло изречение, отговорът не се срязва:
show(space, v, POLICY)1 0.670 можете да върнете стоката до 14 дни от доставката2 0.312 стоката трябва да е в оригиналната опаковка3 0.031 куриер ще я вземе от адреса ви безплатно4 0.874 парите се връщат по картата до пет дни5 0.837 стоки с ваш надпис не се връщат6 0.632 за повреда пишете ни до два дни→ 4Изречението с отговора печели (0,874). Но виж изречение 3: „куриер ще я вземе от адреса ви безплатно“, 0,031. „я“ е стоката от предишното изречение. Само по себе си парчето не казва нито какво взема куриерът, нито че е за връщане. Ако клиентът пита „идва ли куриер за връщането“, това парче губи част от смисъла си.
Затова истинските системи пазят връзката: слагат заглавието на документа и на раздела пред всяко парче, добавят по едно изречение от съседите или режат по абзаци. И пазят към всяко парче откъде е: документ, раздел, място. По това отговорът цитира източника.
Идеята
Кога боли
- Твърде малки парчета губят смисъла: „до 14 дни“ без думата „връщане“ не казва за какво са 14-те дни.
- Твърде големи парчета се размиват като цялата страница и пълнят промпта.
- Застъпването носи повторения. Парчета 5 и 6 имат едни и същи думи. Ако и двете са сред първите k, в промпта влиза повторен текст. Съседни парчета от един документ се сливат, преди да отидат при модела.
- Таблици, списъци и код не се режат по брой думи. Срязаният ред от таблица с цени е безсмислен: такива документи се режат по структурата си.
- Смениш ли рязането, сменяш индекса. Всички парчета се режат и смятат наново.
Няма размер, който е верен за всички документи. Размерът и застъпването се избират с тестов набор от въпроси, както при k в предишния урок: виж оценяването.
По-точно, за любопитните
Размерът в истинските системи. Парчетата се мерят в токени, не в думи: често са от 200 до 800 токена със застъпване от 10 до 20% (примерни числа). Колко токена е един текст, е в урока за токените. Тук парчетата са по 10 думи, а изреченията са почти толкова дълги, затова и застъпването е голямо.
Кога застъпването пази отговора цял. Изречение от L думи е цяло в някое парче, ако L ≤ overlap + 1. С застъпване 4 това са изречения до 5 думи. По-дълго изречение е цяло само ако някой срез падне точно преди него: в упражнението се вижда, че това зависи от местата на срезовете.
Рязане по смисъл. Някои системи режат там, където векторите на съседните изречения се разминават: смяната на темата е граница. Това струва по едно извикване на модела за вектори на изречение.
В Python. words[start:start + size] не пада, когато краят е след последната дума: просто връща по-малко. range(0, len(words), step) дава началата на всички парчета наведнъж.
Упражнение
Проверка на рязането
Реши в редактора си.
Напиши проверка: за парчета по 10 думи и застъпване от 0 до 4 намери най-близкото парче до въпроса и отпечатай колко са парчетата, близостта и дали парчето държи цялото изречение с отговора.
застъпване 0: 5 парчета, 0.815, отговорът е цял: незастъпване 1: 5 парчета, 0.745, отговорът е цял: незастъпване 2: 6 парчета, 0.874, отговорът е цял: дазастъпване 3: 7 парчета, 0.892, отговорът е цял: незастъпване 4: 7 парчета, 0.874, отговорът е цял: даЗастъпване 2 и 4 държат отговора цял, а 1 и 3 не. При застъпване 3 близостта е най-висока (0,892), но парчето свършва на „до пет“ и думата „дни“ е отрязана. По-голяма близост не значи цял отговор. Дали отговорът е цял, зависи от това къде падат срезовете, затова такава проверка се пуска при всяка смяна на рязането.
Още задачи
- Изречения до 16 думи. Слепвай цели изречения в парче, докато не минат 16 думи. Кое парче е най-близо?
- Без повторения. Ако две от първите k парчета се застъпват, слей ги в едно.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Ембединги: смисълът като вектори · Прозорецът на контекста · Цена и бавност: бюджет, кеш, по-малък модел · Търсене по смисъл: най-близките k
- Продължава в: RAG: отговор от документите · Оценяване на RAG: намира ли и стъпва ли · Асистентът на поддръжката
- Задачи по темата: Търсене по смисъл и RAG